Как я (элегантно) переносить текстовое поле поверх метки в определенной части строки?

Я буду кормить несколько строк в ярлыки в Windows Form (я не использую их много). Строки будут похожи на следующие:

"Быстрая коричневая лиса j___ed над собакой l__y"

Я хочу отобразить строку в метке, но накладываю TextBox точно там, где отсутствуют пропущенные буквы.

Там будут 300+ строки, и я ищу простейший, самый элегантный способ сделать это.

Как я могу точно изменить текстовое поле для каждой строки?

EDIT: MaskTextBox не будет работать, поскольку мне нужна многострочная поддержка.

Ответ 1

Чтобы удовлетворить этому требованию, IMO лучше использовать те функции Windows Forms, которые позволяют взаимодействовать с HTML или WPF и Host с помощью WebBrowser управления WebBrowser или WPF ElementHost чтобы показывать контент пользователям. Прежде чем читать этот ответ, пожалуйста, подумайте:

  • Пользователи не должны очищать поля ____. Если они могут очистить их, как только они перейдут на другой пробел, они потеряют возможность найти очищенное поле.
  • Лучше разрешить пользователям использовать клавишу Tab для перемещения между полями ____.
  • Как упоминалось в вопросе: MaskTextBox не будет работать, поскольку мне нужна многострочная поддержка.
  • Как упоминалось в вопросе: будут строки 300+, поэтому смешивание большого количества элементов управления Windows Forms - не очень хорошая идея.

Использование Html в качестве представления модели С# и отображение ее в элементе управления WebBrowser

Здесь я расскажу простой ответ, основанный на показе HTML в элементе управления WebBrowser. В качестве опции вы можете использовать элемент управления WebBrowser и создать подходящий html для отображения в элементе управления WebBrowser с использованием класса режима.

Основная идея - создание html-вывода на основе модели викторины (включая исходный текст и ragnes of blank) и рендеринг модели с использованием html и отображение ее в элементе управления WebBrowser.

Например, используя следующую модель:

quiz = new Quiz();
quiz.Text = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
quiz.Ranges.Add(new SelectionRange(6, 5));
quiz.Ranges.Add(new SelectionRange(30, 7));
quiz.Ranges.Add(new SelectionRange(61, 2));
quiz.Ranges.Add(new SelectionRange(82, 6));

Он будет выдавать этот результат:

fill in the blank - initial

Затем, после ввода пользователем значений, он будет выглядеть следующим образом:

fill in the blank - having answers

И наконец, когда вы нажмете кнопку " Show Result, она отобразит правильные ответы в зеленом цвете и неправильные ответы красного цвета:

fill in the blank - showing results

Код

Вы можете скачать полный рабочий исходный код, например, здесь:

Реализация очень проста:

public class Quiz
{
    public Quiz() { Ranges = new List<SelectionRange>(); }
    public string Text { get; set; }
    public List<SelectionRange> Ranges { get; private set; }
    public string Render()
    {
        /* rendering logic*/
    }
}

Вот полный код класса Quiz:

public class Quiz
{
    public Quiz() { Ranges = new List<SelectionRange>(); }
    public string Text { get; set; }
    public List<SelectionRange> Ranges { get; private set; }
    public string Render()
    {
        var content = new StringBuilder(Text);
        for (int i = Ranges.Count - 1; i >= 0; i--)
        {
            content.Remove(Ranges[i].Start, Ranges[i].Length);
            var length = Ranges[i].Length;
            var replacement = [email protected]"<input id=""q{i}"" 
                type=""text"" class=""editable""
                maxlength=""{length}"" 
                style=""width: {length*1.162}ch;"" />";
            content.Insert(Ranges[i].Start, replacement);
        }
        var result = string.Format(Properties.Resources.Template, content);
        return result;
    }
}

public class SelectionRange
{
    public SelectionRange(int start, int length)
    {
        Start = start;
        Length = length;
    }
    public int Start { get; set; }
    public int Length { get; set; }
}

И вот содержание html-шаблона:

<html>
    <head>
    <meta http-equiv="X-UA-Compatible" content="IE=11" />
    <script>
        function setCorrect(id){{document.getElementById(id).className = 'editable correct';}}
        function setWrong(id){{document.getElementById(id).className = 'editable wrong';}}
    </script>
    <style>
        div {{
            line-height: 1.5;
            font-family: calibri;
        }}
        .editable {{
            border-width: 0px;
            border-bottom: 1px solid #cccccc;
            font-family: monospace;
            display: inline-block;
            outline: 0;
            color: #0000ff;
            font-size: 105%;
        }}
        .editable.correct
        {{    
            color: #00ff00;
            border-bottom: 1px solid #00ff00;
        }}
        .editable.wrong
        {{    
            color: #ff0000;
            border-bottom: 1px solid #ff0000;
        }}
        .editable::-ms-clear {{
            width: 0;
            height: 0;
        }}
    </style>
    </head>
    <body>
    <div>
    {0}
    </div>
    </body>
</html>

Ответ 2

Один из вариантов - использовать текстовое поле Masked.

В вашем примере вы должны установить маску:

"The quick brown fox jLLLed over the l\azy hound"

Что бы выглядело так:

"The quick brown fox j___ed over the lazy hound"

И только разрешите ввести 3 символа (az & AZ) в промежуток. И маска может быть легко изменена с помощью кода.

EDIT: Для удобства...

Ниже приведен список и описание символов маскировки

(взято из http://www.c-sharpcorner.com/uploadfile/mahesh/maskedtextbox-in-C-Sharp/).

0 - Digit, required. Value between 0 and 9.
9 - Digit or space, optional.
# - Digit or space, optional. If this position is blank in the mask, it will be rendered as a space in the Text property.
L - Letter, required. Restricts input to the ASCII letters a-z and A-Z.
? - Letter, optional. Restricts input to the ASCII letters a-z and A-Z.
& - Character, required.
C - Character, optional. Any non-control character.
A - Alphanumeric, required.
a - Alphanumeric, optional.
.  - Decimal placeholder.
, - Thousands placeholder.
: - Time separator.
/ - Date separator.
$ - Currency symbol.
< - Shift down. Converts all characters that follow to lowercase.
> - Shift up. Converts all characters that follow to uppercase.
| - Disable a previous shift up or shift down.
\ - Escape. Escapes a mask character, turning it into a literal. "\\" is the escape sequence for a backslash.

Все остальные персонажи - литералы. Все элементы без маски будут отображаться как сами в MaskedTextBox. Литералы всегда занимают статическую позицию в маске во время выполнения и не могут быть перемещены или удалены пользователем.

Ответ 3

enter image description here

Определите, на какой символ щелкнули, если это был знак подчеркивания, затем размер подчеркивания слева и справа и показать текстовое поле поверх знака подчеркивания.

Вы можете настроить этот код, ярлык - это текстовое поле только для чтения, чтобы получить доступ к GetCharIndexFromPosition и GetPositionFromCharIndex.

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        private System.Windows.Forms.TextBox txtGap;
        private System.Windows.Forms.Label label2;
        private System.Windows.Forms.Label lblClickedOn;
        private System.Windows.Forms.TextBox txtTarget;

        private void txtTarget_MouseDown(object sender, MouseEventArgs e)
        {
            int index = txtTarget.GetCharIndexFromPosition(e.Location);
            //Debugging help
            Point pt = txtTarget.GetPositionFromCharIndex(index);
            lblClickedOn.Text = index.ToString();

            txtGap.Visible = false;

            if (txtTarget.Text[index] == (char)'_')
            {
                //Work out the left co-ordinate for the textbox by checking the number of underscores prior
                int priorLetterToUnderscore = 0;
                for (int i = index - 1; i > -1; i--)
                {
                    if (txtTarget.Text[i] != (char)'_')
                    {
                        priorLetterToUnderscore = i + 1;
                        break;
                    }
                }

                int afterLetterToUnderscore = 0;
                for (int i = index + 1; i <= txtTarget.Text.Length; i++)
                {
                    if (txtTarget.Text[i] != (char)'_')
                    {
                        afterLetterToUnderscore = i;
                        break;
                    }
                }


                //Measure the characters width earlier than the priorLetterToUnderscore
                pt = txtTarget.GetPositionFromCharIndex(priorLetterToUnderscore);
                int left = pt.X + txtTarget.Left;

                pt = txtTarget.GetPositionFromCharIndex(afterLetterToUnderscore);
                int width = pt.X + txtTarget.Left - left;

                //Check the row/line we are on
                SizeF textSize = this.txtTarget.CreateGraphics().MeasureString("A", this.txtTarget.Font, this.txtTarget.Width);
                int line = pt.Y / (int)textSize.Height;

                txtGap.Location = new Point(left, txtTarget.Top + (line * (int)textSize.Height));
                txtGap.Width = width;
                txtGap.Text = string.Empty;
                txtGap.Visible = true;

             }
        }

        private void Form1_Click(object sender, EventArgs e)
        {
            txtGap.Visible = false;
        }

        public Form1()
        {
            this.txtGap = new System.Windows.Forms.TextBox();
            this.label2 = new System.Windows.Forms.Label();
            this.lblClickedOn = new System.Windows.Forms.Label();
            this.txtTarget = new System.Windows.Forms.TextBox();
            this.SuspendLayout();
            // 
            // txtGap
            // 
            this.txtGap.Font = new System.Drawing.Font("Microsoft Sans Serif", 6.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
            this.txtGap.Location = new System.Drawing.Point(206, 43);
            this.txtGap.Name = "txtGap";
            this.txtGap.Size = new System.Drawing.Size(25, 20);
            this.txtGap.TabIndex = 1;
            this.txtGap.Text = "ump";
            this.txtGap.Visible = false;
            // 
            // label2
            // 
            this.label2.AutoSize = true;
            this.label2.Location = new System.Drawing.Point(22, 52);
            this.label2.Name = "label2";
            this.label2.Size = new System.Drawing.Size(84, 13);
            this.label2.TabIndex = 2;
            this.label2.Text = "Char clicked on:";
            // 
            // lblClickedOn
            // 
            this.lblClickedOn.AutoSize = true;
            this.lblClickedOn.Location = new System.Drawing.Point(113, 52);
            this.lblClickedOn.Name = "lblClickedOn";
            this.lblClickedOn.Size = new System.Drawing.Size(13, 13);
            this.lblClickedOn.TabIndex = 3;
            this.lblClickedOn.Text = "_";
            // 
            // txtTarget
            // 
            this.txtTarget.BackColor = System.Drawing.SystemColors.Menu;
            this.txtTarget.BorderStyle = System.Windows.Forms.BorderStyle.None;
            this.txtTarget.Font = new System.Drawing.Font("Microsoft Sans Serif", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
            this.txtTarget.Location = new System.Drawing.Point(22, 21);
            this.txtTarget.Name = "txtTarget";
            this.txtTarget.ReadOnly = true;
            this.txtTarget.Size = new System.Drawing.Size(317, 16);
            this.txtTarget.TabIndex = 4;
            this.txtTarget.Text = "The quick brown fox j___ed over the l__y hound";
            this.txtTarget.MouseDown += new System.Windows.Forms.MouseEventHandler(this.txtTarget_MouseDown);
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(394, 95);
            this.Controls.Add(this.txtGap);
            this.Controls.Add(this.txtTarget);
            this.Controls.Add(this.lblClickedOn);
            this.Controls.Add(this.label2);
            this.Name = "Form1";
            this.Text = "Form1";
            this.Click += new System.EventHandler(this.Form1_Click);
            this.ResumeLayout(false);
            this.PerformLayout();
        }
    }        
}

Чтобы отключить текстовое поле (фальшивая метка): fooobar.com/questions/1444421/...

Редактировать:

Я сделал это для многострочных текстовых полей:

enter image description here

Ответ 4

Это может быть чрезмерным в зависимости от того, насколько сложным вы хотите это быть, но управление веб-браузером winforms (которое по сути является MSIE, запущенным внутри вашего приложения Winforms) может работать как редактор, где вы контролируете, какие части доступны для редактирования.

Загрузите контент с помощью редактируемых частей, помеченных как таковые, например:

<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=10" />
<style>
  span.spEditable { background-color: #f0f0f0; }
</style>
</head>
<body>
<div id="someText">The quick brown fox j<span contenteditable="true" class="spEditable">___</span>ed over the l<span contenteditable="true" class="spEditable">__</span>y hound</div>
</body>
</html>

Ответ 5

Еще одно решение для викторины, использующее класс, унаследованный от TextBox и редактор для отсутствующих букв.

Что делает этот код:
1) Берет элемент управления "Текст метки", список строк (слов) и подстрок тех слов, которые используются в качестве маски, чтобы скрыть некоторые буквы в словах.
2) Создает маску подстрок, используя два пробела Unicode (U+2007 и U+2002) разных размеров, чтобы соответствовать размеру букв для замены
3) Определение размера TextBox без полей ( Editor, объект класса, который наследуется от Textbox) с использованием вычисленных значений Width и Height (в пикселях) подстроки. Устанавливает свойство TextBox.MaxLength длине подстроки.
4) Рассчитывает положение подстрок в тексте многострочной метки, проверяет наличие дублирующихся шаблонов и накладывает объекты Texbox (класс Editor).

Этот метод поддерживает:
Пропорциональные шрифты. Поддерживаются только шрифты Unicode.
Текст меток может занимать несколько строк.

Я использовал шрифт фиксированного размера (Lucida Console) из-за маски символа.
Для работы с пропорциональными шрифтами используются два разных символа маски в зависимости от ширины символов
(т.е. разные символы маски разной ширины, чтобы соответствовать ширине замещаемых символов).

Визуальное представление результатов:
Клавиша TAB используется для перехода от элемента управления TextBox к следующему/предыдущему.
Клавиша ENTER используется для подтверждения редактирования. Затем код проверяет соответствие.
Клавиша ESC сбрасывает текст и показывает начальную маску.

enter image description here

Список слов инициализируется с указанием полного слова и количества смежных символов, которые нужно заменить маской: => jumped: umpe
и связанный элемент управления Label.
Когда класс Quiz инициализируется, он автоматически заменяет все слова в указанном тексте метки маской TextBox.

public class QuizWord
{
    public string Word { get; set; }
    public string WordMask { get; set; }
}

List<Quiz> QuizList = new List<Quiz>();

QuizList.Add(new Quiz(lblSampleText1,
             new List<QuizWord>  
           { new QuizWord { Word = "jumped", WordMask = "umpe" }, 
             new QuizWord { Word = "lazy", WordMask = "az" } }));
QuizList.Add(new Quiz(lblSampleText2,
             new List<QuizWord>  
           { new QuizWord { Word = "dolor", WordMask = "olo" }, 
             new QuizWord { Word = "elit", WordMask = "li" } }));
QuizList.Add(new Quiz(lblSampleText3,
             new List<QuizWord>  
           { new QuizWord { Word = "Brown", WordMask = "row" }, 
             new QuizWord { Word = "Foxes", WordMask = "oxe" }, 
             new QuizWord { Word = "latinorum", WordMask = "atinoru" },
             new QuizWord { Word = "Support", WordMask = "uppor" } }));

Это класс викторины:
Его задача - собрать все редакторы (TextBoxes), которые используются для каждой метки, и рассчитать их местоположение с учетом положения строки, которую они должны заменить в каждом тексте метки.

public class Quiz : IDisposable
{
    private bool _disposed = false;
    private List<QuizWord> _Words = new List<QuizWord>();
    private List<Editor> _Editors = new List<Editor>();
    private MultilineSupport _Multiline;
    private Control _Container = null;

    public Quiz() : this(null, null) { }
    public Quiz(Label RefControl, List<QuizWord> Words)
    {
        this._Container = RefControl.Parent;

        this.Label = null;
        if (RefControl != null)
        {
            this.Label = RefControl;
            this.Matches = new List<QuizWord>();
            if (Words != null)
            {
                this._Multiline = new MultilineSupport(RefControl);
                this.Matches = Words;
            }
        }
    }

    public Label Label { get; set; }
    public List<QuizWord> Matches
    {
        get { return this._Words; }
        set { this._Words = value; Editors_Setup(); }
    }

    private void Editors_Setup()
    {
        if ((this._Words == null) || (this._Words.Count < 1)) return;
        int i = 1;
        foreach (QuizWord _word in _Words)
        {
            List<Point> _Positions = GetEditorsPosition(this.Label.Text, _word);
            foreach (Point _P in _Positions)
            {
                Editor _editor = new Editor(this.Label, _word.WordMask);
                _editor.Location = _P;
                _editor.Name = this.Label.Name + "Editor" + i.ToString(); ++i;
                _Editors.Add(_editor);
                this._Container.Controls.Add(_editor);
                this._Container.Controls[_editor.Name].BringToFront();
            }
        }
    }

    private List<Point> GetEditorsPosition(string _labeltext, QuizWord _word)
    {
        return  Regex.Matches(_labeltext, _word.WordMask) 
                     .Cast<Match>()
                     .Select(t => t.Index).ToList()
                     .Select(idx => this._Multiline.GetPositionFromCharIndex(idx))
                     .ToList();
    }

    private class MultilineSupport
    {
        Label RefLabel;
        float _FontSpacingCoef = 1.8F;
        private TextFormatFlags _flags = TextFormatFlags.SingleLine | TextFormatFlags.Left |
                                         TextFormatFlags.NoPadding | TextFormatFlags.TextBoxControl;

        public MultilineSupport(Label label)
        {
            this.Lines = new List<string>();
            this.LinesFirstCharIndex = new List<int>();
            this.NumberOfLines = 0;
            Initialize(label);
        }

        public int NumberOfLines { get; set; }
        public List<string> Lines { get; set; }
        public List<int> LinesFirstCharIndex { get; set; }

        public int GetFirstCharIndexFromLine(int line)
        {
            if (LinesFirstCharIndex.Count == 0) return -1;
            return LinesFirstCharIndex.Count - 1 >= line ? LinesFirstCharIndex[line] : -1;
        }

        public int GetLineFromCharIndex(int index)
        {
            if (LinesFirstCharIndex.Count == 0) return -1;
            return LinesFirstCharIndex.FindLastIndex(idx => idx <= Index);;
        }

        public Point GetPositionFromCharIndex(int Index)
        {
            return CalcPosition(GetLineFromCharIndex(Index), Index);
        }

        private void Initialize(Label label)
        {
            this.RefLabel = label;
            if (label.Text.Trim().Length == 0)
                return;

            List<string> _wordslist = new List<string>();
            string _substring = string.Empty;
            this.LinesFirstCharIndex.Add(0);
            this.NumberOfLines = 1;
            int _currentlistindex = 0;
            int _start = 0;

            _wordslist.AddRange(label.Text.Split(new char[] { (char)32 }, StringSplitOptions.None));
            foreach (string _word in _wordslist)
            {
                ++_currentlistindex;
                int _wordindex = label.Text.IndexOf(_word, _start);
                int _sublength = MeasureString((_substring + _word + (_currentlistindex < _wordslist.Count ? ((char)32).ToString() : string.Empty)));
                if (_sublength > label.Width)
                {
                    this.Lines.Add(_substring);
                    this.LinesFirstCharIndex.Add(_wordindex);
                    this.NumberOfLines += 1;
                    _substring = string.Empty;
                }
                _start += _word.Length + 1;
                _substring += _word + (char)32;
            }
            this.Lines.Add(_substring.TrimEnd());
        }

        private Point CalcPosition(int Line, int Index)
        {
            int _font_padding = (int)((RefLabel.Font.Size - (int)(RefLabel.Font.Size % 12)) * _FontSpacingCoef);
            int _verticalpos = Line * this.RefLabel.Font.Height + this.RefLabel.Top;
            int _horizontalpos = MeasureString(this.Lines[Line].Substring(0, Index - GetFirstCharIndexFromLine(Line)));
            return new Point(_horizontalpos + _font_padding, _verticalpos);
        }

        private int MeasureString(string _string)
        {
            return TextRenderer.MeasureText(RefLabel.CreateGraphics(), _string,
                                            this.RefLabel.Font, this.RefLabel.Size, _flags).Width;
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected void Dispose(bool IsSafeDisposing)
    {
        if (IsSafeDisposing && (!this._disposed))
        {
            foreach (Editor _editor in _Editors)
                if (_editor != null) _editor.Dispose();
            this._disposed = true;
        }
    }
}

Это класс Editor (наследуется от TextBox):
Он строит и вычисляет длину символов маски и автоматически изменяет размеры, используя это значение.
Имеет базовые возможности редактирования.

public class Editor : TextBox
{
    private string SubstChar = string.Empty;
    private string SubstCharLarge = ((char)0x2007).ToString();
    private string SubstCharSmall = ((char)0x2002).ToString();
    private Font NormalFont = null;
    private Font UnderlineFont = null;
    private string WordMask = string.Empty;
    private TextFormatFlags _flags = TextFormatFlags.NoPadding | TextFormatFlags.Left |
                                     TextFormatFlags.Bottom | TextFormatFlags.WordBreak |
                                     TextFormatFlags.TextBoxControl;

    public Editor(Label RefLabel, string WordToMatch)
    {
        this.BorderStyle = BorderStyle.None;
        this.TextAlign = HorizontalAlignment.Left;
        this.Margin = new Padding(0);

        this.MatchWord = WordToMatch;
        this.MaxLength = WordToMatch.Length;
        this._Label = RefLabel;
        this.NormalFont = RefLabel.Font;
        this.UnderlineFont = new Font(RefLabel.Font, (RefLabel.Font.Style | FontStyle.Underline));
        this.Font = this.UnderlineFont;
        this.Size = GetTextSize(WordToMatch);
        this.WordMask = CreateMask(this.Size.Width);
        this.Text = this.WordMask;
        this.BackColor = RefLabel.BackColor;
        this.ForeColor = RefLabel.ForeColor;

        this.KeyDown += this.KeyDownHandler;
        this.Enter += (sender, e) => { this.Font = this.UnderlineFont; this.SelectionStart = 0;  this.SelectionLength = 0; };
        this.Leave += (sender, e) => { CheckWordMatch(); };
    }

    public string MatchWord { get; set; }
    private Label _Label { get; set; }

    public void KeyDownHandler(object sender, KeyEventArgs e)
    {
        int _start = this.SelectionStart;
        switch (e.KeyCode)
        {
        case Keys.Back:
            if (this.SelectionStart > 0)
            {
                this.AppendText(SubstChar);
                this.SelectionStart = 0;
                this.ScrollToCaret();
            }
            this.SelectionStart = _start;
            break;
        case Keys.Delete:
            if (this.SelectionStart < this.Text.Length)
            {
                this.AppendText(SubstChar);
                this.SelectionStart = 0;
                this.ScrollToCaret();
            }
            this.SelectionStart = _start;
            break;
        case Keys.Enter:
            e.SuppressKeyPress = true;
            CheckWordMatch();
            break;
        case Keys.Escape:
            e.SuppressKeyPress = true;
            this.Text = this.WordMask;
            this.ForeColor = this._Label.ForeColor;
            break;
        default:
            if ((e.KeyCode >= (Keys)32 & e.KeyCode <= (Keys)127) && (e.KeyCode < (Keys)36 | e.KeyCode > (Keys)39))
            {
                int _removeat = this.Text.LastIndexOf(SubstChar);
                if (_removeat > -1) this.Text = this.Text.Remove(_removeat, 1);
                this.SelectionStart = _start;
            }
            break;
        }
    }
    private void CheckWordMatch()
    {
        if (this.Text != this.WordMask) {
            this.Font = this.Text == this.MatchWord ? this.NormalFont : this.UnderlineFont;
            this.ForeColor = this.Text == this.MatchWord ? Color.Green : Color.Red;
        } else {
            this.ForeColor = this._Label.ForeColor;
        }
    }

    private Size GetTextSize(string _mask)
    {
        return TextRenderer.MeasureText(this._Label.CreateGraphics(), _mask, this._Label.Font, this._Label.Size, _flags);
    }

    private string CreateMask(int _EditorWidth)
    {
        string _TestMask = new StringBuilder().Insert(0, SubstCharLarge, this.MatchWord.Length).ToString();
        SubstChar = (GetTextSize(_TestMask).Width <= _EditorWidth) ? SubstCharLarge : SubstCharSmall;
        return SubstChar == SubstCharLarge 
                          ? _TestMask  
                          : new StringBuilder().Insert(0, SubstChar, this.MatchWord.Length).ToString();
    }
}

Ответ 6

Подумайте об использовании комбинации столбцов DataGridView и Masked Cell.

В Edit Control Showing вы измените маску этой конкретной строки.

Вот пример использования кода, который включает в себя сетку и уникальную маскировку для каждой строки.

Public Class Form1
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load

        Dim mec As New MaskedEditColumn
        mec.Mask = ""
        mec.DataPropertyName = "Data"
        Me.DataGridView1.Columns.Add(mec)


        Dim tbl As New Data.DataTable
        tbl.Columns.Add("Data")
        tbl.Columns.Add("Mask")
        tbl.Rows.Add(New Object() {"The quick brown fox j   ed over the lazy hound", "The quick brown fox jaaaed over the l\azy hound"})
        tbl.Rows.Add(New Object() {"    quick brown fox j   ed over the lazy hound", "aaa quick brown fox jaaaed over the l\azy hound"})
        tbl.Rows.Add(New Object() {"The       brown fox j   ed over the lazy hound", "The aaaaa brown fox jaaaed over the l\azy hound"})
        Me.DataGridView1.AutoGenerateColumns = False
        Me.DataGridView1.DataSource = tbl
    End Sub

    Private Sub DataGridView1_EditingControlShowing(sender As Object, e As DataGridViewEditingControlShowingEventArgs) Handles DataGridView1.EditingControlShowing

        If e.Control.GetType().Equals(GetType(MaskedEditingControl)) Then

            Dim mec As MaskedEditingControl = e.Control
            Dim row As DataGridViewRow = Me.DataGridView1.CurrentRow
            mec.Mask = row.DataBoundItem("Mask")

        End If

    End Sub
End Class

И столбец сетки, полученный отсюда: http://www.vb-tips.com/MaskedEditColumn.aspx

Public Class MaskedEditColumn
    Inherits DataGridViewColumn
    Public Sub New()
        MyBase.New(New MaskedEditCell())
    End Sub
    Public Overrides Property CellTemplate() As DataGridViewCell
        Get
            Return MyBase.CellTemplate
        End Get
        Set(ByVal value As DataGridViewCell)

            ' Ensure that the cell used for the template is a CalendarCell.
            If Not (value Is Nothing) AndAlso
                Not value.GetType().IsAssignableFrom(GetType(MaskedEditCell)) _
                Then
                Throw New InvalidCastException("Must be a MaskedEditCell")
            End If
            MyBase.CellTemplate = value
        End Set
    End Property
    Private m_strMask As String
    Public Property Mask() As String
        Get
            Return m_strMask
        End Get
        Set(ByVal value As String)
            m_strMask = value
        End Set
    End Property
    Private m_tyValidatingType As Type
    Public Property ValidatingType() As Type
        Get
            Return m_tyValidatingType
        End Get
        Set(ByVal value As Type)
            m_tyValidatingType = value
        End Set
    End Property
    Private m_cPromptChar As Char = "_"c
    Public Property PromptChar() As Char
        Get
            Return m_cPromptChar
        End Get
        Set(ByVal value As Char)
            m_cPromptChar = value
        End Set
    End Property
    Private ReadOnly Property MaskedEditCellTemplate() As MaskedEditCell
        Get
            Return TryCast(Me.CellTemplate, MaskedEditCell)
        End Get
    End Property
End Class
Public Class MaskedEditCell
    Inherits DataGridViewTextBoxCell
    Public Overrides Sub InitializeEditingControl(ByVal rowIndex As Integer,
        ByVal initialFormattedValue As Object,
        ByVal dataGridViewCellStyle As DataGridViewCellStyle)
        ' Set the value of the editing control to the current cell value.
        MyBase.InitializeEditingControl(rowIndex, initialFormattedValue,
            dataGridViewCellStyle)
        Dim mecol As MaskedEditColumn = DirectCast(OwningColumn, MaskedEditColumn)
        Dim ctl As MaskedEditingControl =
            CType(DataGridView.EditingControl, MaskedEditingControl)
        Try
            ctl.Text = Me.Value.ToString
        Catch
            ctl.Text = ""
        End Try
        ctl.Mask = mecol.Mask
        ctl.PromptChar = mecol.PromptChar
        ctl.ValidatingType = mecol.ValidatingType
    End Sub
    Public Overrides ReadOnly Property EditType() As Type
        Get
            ' Return the type of the editing contol that CalendarCell uses.
            Return GetType(MaskedEditingControl)
        End Get
    End Property
    Public Overrides ReadOnly Property ValueType() As Type
        Get
            ' Return the type of the value that CalendarCell contains.
            Return GetType(String)
        End Get
    End Property
    Public Overrides ReadOnly Property DefaultNewRowValue() As Object
        Get
            ' Use the current date and time as the default value.
            Return ""
        End Get
    End Property
    Protected Overrides Sub Paint(ByVal graphics As System.Drawing.Graphics, ByVal clipBounds As System.Drawing.Rectangle, ByVal cellBounds As System.Drawing.Rectangle, ByVal rowIndex As Integer, ByVal cellState As System.Windows.Forms.DataGridViewElementStates, ByVal value As Object, ByVal formattedValue As Object, ByVal errorText As String, ByVal cellStyle As System.Windows.Forms.DataGridViewCellStyle, ByVal advancedBorderStyle As System.Windows.Forms.DataGridViewAdvancedBorderStyle, ByVal paintParts As System.Windows.Forms.DataGridViewPaintParts)
        MyBase.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts)
    End Sub
End Class
Class MaskedEditingControl
    Inherits MaskedTextBox
    Implements IDataGridViewEditingControl
    Private dataGridViewControl As DataGridView
    Private valueIsChanged As Boolean = False
    Private rowIndexNum As Integer
    Public Property EditingControlFormattedValue() As Object _
        Implements IDataGridViewEditingControl.EditingControlFormattedValue
        Get
            Return Me.Text
        End Get
        Set(ByVal value As Object)
            Me.Text = value.ToString
        End Set
    End Property
    Public Function EditingControlWantsInputKey(ByVal key As Keys,
           ByVal dataGridViewWantsInputKey As Boolean) As Boolean _
           Implements IDataGridViewEditingControl.EditingControlWantsInputKey
        Return True
    End Function
    Public Function GetEditingControlFormattedValue(ByVal context _
        As DataGridViewDataErrorContexts) As Object _
        Implements IDataGridViewEditingControl.GetEditingControlFormattedValue
        Return Me.Text
    End Function
    Public Sub ApplyCellStyleToEditingControl(ByVal dataGridViewCellStyle As _
        DataGridViewCellStyle) _
        Implements IDataGridViewEditingControl.ApplyCellStyleToEditingControl

        Me.Font = dataGridViewCellStyle.Font
        Me.ForeColor = dataGridViewCellStyle.ForeColor
        Me.BackColor = dataGridViewCellStyle.BackColor
    End Sub
    Public Property EditingControlRowIndex() As Integer _
        Implements IDataGridViewEditingControl.EditingControlRowIndex
        Get
            Return rowIndexNum
        End Get
        Set(ByVal value As Integer)
            rowIndexNum = value
        End Set
    End Property
    Public Sub PrepareEditingControlForEdit(ByVal selectAll As Boolean) _
        Implements IDataGridViewEditingControl.PrepareEditingControlForEdit
        ' No preparation needs to be done.
    End Sub
    Public ReadOnly Property RepositionEditingControlOnValueChange() _
        As Boolean Implements _
        IDataGridViewEditingControl.RepositionEditingControlOnValueChange
        Get
            Return False
        End Get
    End Property
    Public Property EditingControlDataGridView() As DataGridView _
        Implements IDataGridViewEditingControl.EditingControlDataGridView
        Get
            Return dataGridViewControl
        End Get
        Set(ByVal value As DataGridView)
            dataGridViewControl = value
        End Set
    End Property

    Public Property EditingControlValueChanged() As Boolean _
        Implements IDataGridViewEditingControl.EditingControlValueChanged
        Get
            Return valueIsChanged
        End Get
        Set(ByVal value As Boolean)
            valueIsChanged = value
        End Set
    End Property
    Public ReadOnly Property EditingControlCursor() As Cursor _
        Implements IDataGridViewEditingControl.EditingPanelCursor
        Get
            Return MyBase.Cursor
        End Get
    End Property
    Protected Overrides Sub OnTextChanged(ByVal e As System.EventArgs)
        ' Notify the DataGridView that the contents of the cell have changed.
        valueIsChanged = True
        Me.EditingControlDataGridView.NotifyCurrentCellDirty(True)
        MyBase.OnTextChanged(e)
    End Sub
End Class

Ответ 7

Вот как я подхожу к нему. Разделите с регулярным выражением строку и создайте отдельные метки для каждой из подстрок. Поместите все метки в FlowLayoutPanel. Когда щелкнуть метку, удалите ее и в той же позиции добавьте редактирование TextBox. Когда фокус потерян (или нажата клавиша ввода), удалите TextBox и верните метку; установите текст метки в текст TextBox.

Сначала создайте пользовательский UserControl следующим образом:

public partial class WordEditControl : UserControl
{
    private readonly Regex underscoreRegex = new Regex("(__*)");
    private List<EditableLabel> labels = new List<EditableLabel>();

    public WordEditControl()
    {
        InitializeComponent();
    }

    public void SetQuizText(string text)
    {
        contentPanel.Controls.Clear();
        foreach (string item in underscoreRegex.Split(text))
        {
            var label = new Label
            {
                FlatStyle = FlatStyle.System,
                Padding = new Padding(),
                Margin = new Padding(0, 3, 0, 0),
                TabIndex = 0,
                Text = item,
                BackColor = Color.White,
                TextAlign = ContentAlignment.TopCenter
            };
            if (item.Contains("_"))
            {
                label.ForeColor = Color.Red;
                var edit = new TextBox
                {
                    Margin = new Padding()
                };
                labels.Add(new EditableLabel(label, edit));

            }
            contentPanel.Controls.Add(label);
            using (Graphics g = label.CreateGraphics())
            {
                SizeF textSize = g.MeasureString(item, label.Font);
                label.Size = new Size((int)textSize.Width - 4, (int)textSize.Height);
            }
        }
    }

    // Copied it from the .Designer file for the sake of completeness
    private void InitializeComponent()
    {
        this.contentPanel = new System.Windows.Forms.FlowLayoutPanel();
        this.SuspendLayout();
        // 
        // contentPanel
        // 
        this.contentPanel.Dock = System.Windows.Forms.DockStyle.Fill;
        this.contentPanel.Location = new System.Drawing.Point(0, 0);
        this.contentPanel.Name = "contentPanel";
        this.contentPanel.Size = new System.Drawing.Size(150, 150);
        this.contentPanel.TabIndex = 0;
        // 
        // WordEditControl
        // 
        this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
        this.Controls.Add(this.contentPanel);
        this.Name = "WordEditControl";
        this.ResumeLayout(false);

    }

    private System.Windows.Forms.FlowLayoutPanel contentPanel;
}

Этот принимает текст викторины, затем разбивает его на регулярное выражение и создает метки и текстовые поля. Если вам интересно узнать, как заставить Regex возвращать матчи, а не только подстроки, посмотрите здесь

Затем, чтобы позаботиться о переходе между редактированием, я создал класс EditableLabel. Похоже, это

class EditableLabel
{
    private string originalText;
    private Label label;
    private TextBox editor;

    public EditableLabel(Label label, TextBox editor)
    {
        this.label = label ?? throw new ArgumentNullException(nameof(label));
        this.editor = editor ?? throw new ArgumentNullException(nameof(editor));
        originalText = label.Text;

        using (Graphics g = label.CreateGraphics())
        {
            this.editor.Width = (int)g.MeasureString("M", this.editor.Font).Width * label.Text.Length;
        }

        editor.LostFocus += (s, e) => SetText();
        editor.KeyUp += (s, e) =>
        {
            if (e.KeyCode == Keys.Enter)
            {
                SetText();
            }
        };

        label.Click += (s, e) =>
        {
            Swap(label, editor);
            this.editor.Focus();
        };
    }

    private void SetText()
    {
        Swap(editor, label);
        string editorText = editor.Text.Trim();
        label.Text = editorText.Length == 0 ? originalText : editorText;
        using (Graphics g = label.CreateGraphics())
        {
            SizeF textSize = g.MeasureString(label.Text, label.Font);
            label.Width = (int)textSize.Width - 4;
        }
    }

    private void Swap(Control original, Control replacement)
    {
        var panel = original.Parent;
        int index = panel.Controls.IndexOf(original);
        panel.Controls.Remove(original);
        panel.Controls.Add(replacement);
        panel.Controls.SetChildIndex(replacement, index);
    }
}

Вы можете использовать пользовательский UserControl, перетащив его из конструктора (после успешного создания) или добавьте его следующим образом:

public partial class Form1 : Form
{
    private WordEditControl wordEditControl1;
    public Form1()
    {
        InitializeComponent();
        wordEditControl1 = new WordEditControl();
        wordEditControl1.SetQuizText("The quick brown fox j___ed over the l__y hound");
        Controls.Add(wordEditControl1)
    }
}

Конечный результат будет выглядеть так:

Word quiz form

Плюсы и минусы

Что я считаю хорошим с этим решением:

  • он гибкий, поскольку вы можете уделять особое внимание редактируемому ярлыку. Вы можете изменить свой цвет, как я здесь, поставить контекстное меню с такими действиями, как "Очистить", "Оценить", "Показать ответ" и т.д.

  • Он почти многострочный. Панель макета потока заботится об упаковке компонентов, и она будет работать, если в строке викторины будут частые перерывы. В противном случае у вас будет очень большая метка, которая не будет вписываться в панель. Вы можете использовать трюк, чтобы обойти это, и использовать \n для разрыва длинных строк. Вы можете обрабатывать \n в SetQuizText() но я оставлю это вам :) Имейте в виду, что идентификатор, который вы не обрабатываете, будет делать ярлык, и это не будет хорошо связываться с FlowLayoutPanel.

  • TextBoxs может поместиться лучше. Текстовое поле редактирования, которое будет соответствовать 3-м символам, не будет иметь то же значение, что и метка с тремя символами. С этим решением вам не нужно это беспокоиться. После того как отредактированная метка будет заменена текстовым полем, следующие элементы управления сдвинутся вправо, чтобы они соответствовали текстовому полю. Как только метка вернется, другие элементы управления могут перестраиваться.

Однако мне не нравится, что все это будет стоить дорого: вам нужно вручную выровнять элементы управления. Вот почему вы видите некоторые магические числа (которые мне не нравятся и стараются их избежать). Текстовое поле не имеет той же высоты, что и метка. Вот почему я заполнил все ярлыки 3 пикселями в верхней части. Также по какой-то причине, что у меня нет времени на исследование, MeasureString() не возвращает точную ширину, она немного шире. С пробкой и ошибкой я понял, что удаление 4 пикселей лучше выравнивает метки

Теперь вы говорите, что будет 300 строк, поэтому, я думаю, вы имеете в виду 300 "quites". Если они такие же маленькие, как у быстрой коричневой лисы, я думаю, что способ, которым обрабатывается многострочный цикл в моем решении, не вызовет у вас никаких проблем. Но если текст будет больше, я предлагаю вам пойти с одним из других ответов, которые работают с многострочными текстовыми полями.

Имейте в виду, что если это будет становиться более сложным, как, например, причудливые индикаторы, что текст был прав или неправильным, или если вы хотите, чтобы элемент управления реагировал на изменения размера, тогда вам понадобятся текстовые элементы управления, которые не предоставляются каркасом. Библиотека форм Windows, к сожалению, оставалась застойной уже несколько лет, и изящные решения в таких проблемах, как ваша, трудно найти, по крайней мере, без коммерческого контроля.

Надеюсь, это поможет вам начать работу.

Ответ 8

Я разработал немного более легкое решение, чтобы понять, что может помочь вам приступить к работе по крайней мере (у меня не было времени играть с несколькими входами на одном ярлыке, но я правильно его работал для 1).

private void Form1_Load()
{
            for (var i = 0; i < 20; i++)
            {
                Label TemporaryLabel = new Label();
                TemporaryLabel.AutoSize = false;
                TemporaryLabel.Size = new Size(flowLayoutPanel1.Width, 50);
                TemporaryLabel.Text = "This is a ______ message";
                string SubText = "";
                var StartIndex = TemporaryLabel.Text.IndexOf('_');
                var EndIndex = TemporaryLabel.Text.LastIndexOf('_');
                if ((StartIndex != -1 && EndIndex != -1) && EndIndex > StartIndex)
                {
                    string SubString = TemporaryLabel.Text.Substring(StartIndex, EndIndex - StartIndex);
                    SizeF nSize = Measure(SubString);
                    TextBox TemporaryBox = new TextBox();
                    TemporaryBox.Size = new Size((int)nSize.Width, 50);
                    TemporaryLabel.Controls.Add(TemporaryBox);
                    TemporaryBox.Location = new Point(TemporaryBox.Location.X + (int)Measure(TemporaryLabel.Text.Substring(0, StartIndex - 2)).Width, TemporaryBox.Location.Y);
                }
                else continue;
                flowLayoutPanel1.Controls.Add(TemporaryLabel);
            }
} 

EDIT: Забыл включить метод "Мера":

private SizeF Measure(string Data)
{
    using (var BMP = new Bitmap(1, 1))
    {
        using (Graphics G = Graphics.FromImage(BMP))
        {
            return G.MeasureString(Data, new Font("segoe ui", 11, FontStyle.Regular));
        }
    }
}

Результат:

enter image description here

Затем вы должны иметь возможность назначать обработчики событий отдельным текстовым полям/имён для более легкого доступа позже, когда пользователь взаимодействует с данным вводом.

Ответ 9

Я бы попробовал что-то вроде этого (наверняка понадобится корректировка размеров):

    var indexOfCompletionString = label.Text.IndexOf("____");
    var labelLeftPos = label.Left;
    var labelTopPos =  label.Top;

    var completionStringMeasurments = this.CreateGraphics().MeasureString("____", label.Font);
    var substr = label.Text.Substring(0, indexOfCompletionString);
    var substrMeasurments =  this.CreateGraphics().MeasureString(substr, label.Font);

    var tBox = new TextBox
    {
        Height = (int)completionStringMeasurments.Height,
        Width = (int)completionStringMeasurments.Width,
        Location = new Point(labelLeftPos + (int)substrMeasurments.Width, labelTopPos)
    };

    tBox.BringToFront();
    Controls.Add(tBox);
    Controls.SetChildIndex(tBox, 0);

Ответ 10

    Private Sub MainForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    Me.Controls.Add(New TestTextBox With {.Text = "The quick brown fox j___ed over the l__y hound", .Dock = DockStyle.Fill, .Multiline = True})
End Sub



Public Class TestTextBox
    Inherits Windows.Forms.TextBox
    Protected Overrides Sub OnKeyDown(e As KeyEventArgs)
        Dim S = Me.SelectionStart
        Me.SelectionStart = ReplceOnlyWhatNeeded(Me.SelectionStart, (Chr(e.KeyCode)))
        e.SuppressKeyPress = True ' Block Evrything 
    End Sub


    Public Overrides Property Text As String
        Get
            Return MyBase.Text
        End Get
        Set(value As String)
            'List Of Editable Symbols 
            ValidIndex.Clear()
            For x = 0 To value.Length - 1
                If value(x) = DefaultMarker Then ValidIndex.Add(x)
            Next
            MyBase.Text = value
            Me.SelectionStart = Me.ValidIndex.First
        End Set
    End Property

    '---------------------------------------
    Private DefaultMarker As Char = "_"
    Private ValidIndex As New List(Of Integer)
    Private Function ReplceOnlyWhatNeeded(oPoz As Integer, oInputChar As Char) As Integer
        'Replece one symbol in string at pozition, in case delete put default marker
        If Me.ValidIndex.Contains(Me.SelectionStart) And (Char.IsLetter(oInputChar) Or Char.IsNumber(oInputChar)) Then
            MyBase.Text = MyBase.Text.Insert(Me.SelectionStart, oInputChar).Remove(Me.SelectionStart + 1, 1) ' Replece in Output String new symbol
        ElseIf Me.ValidIndex.Contains(Me.SelectionStart) And Asc(oInputChar) = 8 Then
            MyBase.Text = MyBase.Text.Insert(Me.SelectionStart, DefaultMarker).Remove(Me.SelectionStart + 1, 1) ' Add Blank Symbol when backspace
        Else
            Return Me.ValidIndex.First  'Avrything else not allow
        End If
        'Return Next Point to edit 
        Dim Newpoz As Integer? = Nothing
        For Each x In Me.ValidIndex
            If x > oPoz Then
                Return x
                Exit For
            End If
        Next
        Return Me.ValidIndex.First
    End Function

End Class

U Dont Need Label и текстовое поле для этого, вы можете делать это на любом дисплее в любом строковом управлении. Только вам нужна позиция ввода пользователя, строка, которую хотите изменить с символами в качестве держателя места и символа ввода, является образцом в текстовом поле, при ключевом вводе, поэтому количество элементов управления не импортируется. Для длинной строки копия u всегда может быть для каждого символа.