Come posso (elegantemente) trasporre la casella di testo sull’etichetta in una parte specifica della stringa?

Inserirò un numero di stringhe in etichette su un Windows Form (non le uso molto). Le stringhe saranno simili alle seguenti:

“La volpe marrone veloce j___ed sopra il lyy hound”

Voglio mostrare la stringa in un’etichetta ma sovrapporre un TextBox esattamente dove sono le lettere mancanti.

Ci saranno più di 300 stringhe e sto cercando il modo più semplice ed elegante per farlo.

Come riposiziono la casella di testo in modo preciso per ogni stringa?

EDIT: Un MaskTextBox non funzionerà in quanto ho bisogno di supporto multilinea.

Per soddisfare questo requisito, IMO è meglio utilizzare le funzionalità di Windows Form che consentono l’interoperabilità con HTML o WPF e il controllo Host di un WebBrowser o un ElementHost WPF per mostrare il contenuto agli utenti. Prima di leggere questa risposta, ti preghiamo di considerare:

  • Gli utenti non dovrebbero essere in grado di cancellare i campi ____ . Se riescono a eliminarli, una volta spostati in un altro spazio, perderanno la possibilità di trovare il campo libero.
  • È meglio consentire agli utenti di utilizzare il tasto Tab per spostarsi tra i campi ____ .
  • Come è menzionato nella domanda: un MaskTextBox non funzionerà in quanto ho bisogno del supporto multilinea.
  • Come menzionato nella domanda: ci saranno più di 300 stringhe quindi mescolare un sacco di controllo di Windows Form non è una buona idea.

Utilizzo di Html come visualizzazione di un modello C # e mostrarlo nel controllo WebBrowser

Qui condividerò una risposta semplice basata sulla visualizzazione di HTML nel controllo WebBrowser . Come opzione è ansible utilizzare un controllo WebBrowser e creare un html adatto da mostrare nel controllo WebBrowser utilizzando una class mode.

L’idea principale è la creazione di un output html basato sul modello del quiz (incluso il testo originale e ragnes of blanks) e il rendering del modello usando html e mostrandolo in un controllo WebBrowser .

Ad esempio utilizzando il seguente modello:

 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)); 

Renderà questo output:

riempi lo spazio vuoto - iniziale

Quindi, dopo che l’utente ha inserito i valori, verrà mostrato in questo modo:

riempi lo spazio vuoto - avendo risposte

E alla fine, quando fai clic sul pulsante Show Result , mostrerà le risposte corrette in verde e le risposte sbagliate in rosso:

riempi lo spazio vuoto - mostrando i risultati

Codice

Puoi scaricare il codice sorgente funzionante completo per esempio qui:

  • R-Aghaei / FillInTheBlankQuizSample

L’implementazione è semplice e tranquilla:

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

Ecco il codice completo della class Quiz :

 public class Quiz { public Quiz() { Ranges = new List(); } public string Text { get; set; } public List 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]""; 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; } } 

Ed ecco il contenuto del modello html:

        
{0}

Un’opzione è usare una casella di testo mascherata.

Nel tuo esempio, devi impostare la maschera su:

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

Che apparirebbe come:

 "The quick brown fox j___ed over the lazy hound" 

E consentire solo 3 caratteri (az e AZ) da inserire nello spazio. E la maschera potrebbe essere facilmente modificata tramite codice.

MODIFICA: per comodità …

Ecco una lista e una descrizione dei personaggi mascherati

(tratto da 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 az and AZ. ? - Letter, optional. Restricts input to the ASCII letters az and AZ. & - 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. 

Tutti gli altri personaggi – letterali. Tutti gli elementi non maschera appariranno come se stessi all’interno di MaskedTextBox. I letterali occupano sempre una posizione statica nella maschera in fase di esecuzione e non possono essere spostati o eliminati dall’utente.

inserisci la descrizione dell'immagine qui

Calcola il personaggio su cui è stato fatto clic, se era un trattino basso, quindi ridimensiona i caratteri di sottolineatura a sinistra e a destra e mostra una casella di testo in cima ai caratteri di sottolineatura.

È ansible modificare questo codice, l’etichetta è in realtà una casella di testo di sola lettura per ottenere l’accesso ai metodi GetCharIndexFromPosition e 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(); } } } 

Per disabilitare la casella di testo (etichetta falsa) dalla selezione: https://stackoverflow.com/a/42391380/495455

Modificare:

L'ho fatto funzionare per le caselle di testo multilinea:

inserisci la descrizione dell'immagine qui

Questo può essere eccessivo a seconda di quanto sia complesso ciò che si desidera, ma un controllo del browser Web di Winform (che è essenzialmente MSIE in esecuzione all’interno della propria app Winforms) può funzionare come un editor in cui si controllano quali parti sono modificabili.

Carica i tuoi contenuti con le parti modificabili contrassegnate come tali, ad esempio:

       
The quick brown fox j___ed over the l__y hound

Un altro, usando un semplice controllo TextBox.

EDIT1: aggiunto il supporto per i font proporzionali. Sono supportati solo i caratteri Unicode.
EDIT2: eliminato il segnaposto Underscore, ora usando 2 caratteri unicode e sottolineatura carattere. Aggiunto supporto IDisposable.
EDIT3: aggiunto il supporto multilinea

Cosa fa questo codice:
1) Prende una lista di stringhe (parole) e sottostringhe di quelle parole e il testo di un controllo Label
2) Crea una maschera delle sottostringhe usando due caratteri di spazio Unicode (U + 2007 e U + 2002) di dimensioni diverse, per adattarsi alla dimensione delle lettere da sostituire
3) Ridimensiona un TextBox senza bordo (un object di class che eredita da Textbox) utilizzando la larghezza e l’altezza calcolate (in pixel) della sottostringa. Imposta la proprietà MaxLength sulla lunghezza della sottostringa.
4) Calcola la posizione delle sottostringhe all’interno di un testo di etichetta multilinea, verifica la presenza di schemi duplicati e sovrappone gli oggetti Texbox (class Editor)

Ho usato un font a dimensione fissa (Lucida Console) a causa del carattere maschera .
Per gestire i caratteri proporzionali, vengono utilizzati due diversi caratteri maschera, a seconda della larghezza dei caratteri
(cioè caratteri di maschera diversi di larghezza diversa per abbinare la larghezza dei caratteri sostituiti).

Una rappresentazione visiva dei risultati:
Il tasto TAB viene utilizzato per passare da un controllo TextBox al successivo / precedente.
Il tasto INVIO viene utilizzato per accettare la modifica. Quindi il codice controlla se è una corrispondenza.
Il tasto ESC reimposta il testo e mostra la maschera iniziale.

inserisci la descrizione dell'immagine qui

Un elenco di parole viene inizializzato specificando una parola completa e un numero di caratteri contigui da sostituire con una maschera: => jumped : umpe
e il controllo Label associato.
Quando una class Quiz viene inizializzata, sottotitola automaticamente tutte le parole nel testo Etichetta specificato con una maschera TextBox.

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

Questa è la class Quiz:
Il suo compito è quello di raccogliere tutti gli Editors (TextBox) che vengono utilizzati per ciascuna etichetta e calcolare la loro posizione, data la posizione della stringa che devono sostituire in ciascun testo Label.

 public class Quiz : IDisposable { private bool _disposed = false; private List _Words = new List(); private List _Editors = new List(); private MultilineSupport _Multiline; private Control _Container = null; public Quiz() : this(null, null) { } public Quiz(Label RefControl, List Words) { this._Container = RefControl.Parent; this.Label = null; if (RefControl != null) { this.Label = RefControl; this.Matches = new List(); if (Words != null) { this._Multiline = new MultilineSupport(RefControl); this.Matches = Words; } } } public Label Label { get; set; } public List 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 _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 GetEditorsPosition(string _labeltext, QuizWord _word) { return Regex.Matches(_labeltext, _word.WordMask) .Cast() .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(); this.LinesFirstCharIndex = new List(); this.NumberOfLines = 0; Initialize(label); } public int NumberOfLines { get; set; } public List Lines { get; set; } public List 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 _wordslist = new List(); 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; } } } 

Questa è la class Editor (ereditata da TextBox):
Costruisce e calcola la lunghezza dei caratteri della maschera e si autosoma utilizzando questo valore.
Ha capacità di editing di base.

 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(); } } 

Prendi in considerazione l’utilizzo di una combinazione di una colonna DataGridView e una colonna Masked Cell.

In Modifica controllo visualizzato, cambieresti la maschera di quella particolare riga.

Ecco alcuni esempi di utilizzo del codice che includono la griglia e il mascheramento unico per ogni riga.

 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 

E la colonna della griglia, proveniente da qui: 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 

Questo è il modo in cui mi avvicinerei. Dividi con espressioni regolari la stringa e crea etichette separate per ciascuna sottostringa. Metti tutte le etichette in un FlowLayoutPanel . Quando si fa clic su un’etichetta, rimuoverla e nella stessa posizione aggiungere il TextBox di modifica. Quando la messa a fuoco viene persa (o viene premuto Invio) rimuovere il TextBox e rimettere l’etichetta; imposta il testo dell’etichetta sul testo del TextBox.

Per prima cosa creare UserControl personalizzato come il seguente

 public partial class WordEditControl : UserControl { private readonly Regex underscoreRegex = new Regex("(__*)"); private List labels = new List(); 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; } 

This one accepts the quiz text then splits it with regex and creates the labels and the text boxes. If you are interested to know how to make the Regex return the matches and not only the substrings have a look here

Then in order to take care of the transition between editing, I created an EditableLabel class. Sembra così

 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); } } 

You can use the custom UserControl by drag and dropping it from the designer (after you successfully build) or add it like this:

 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) } } 

The end result will look like that:

Word quiz form

Pro e contro

What I consider good with this solution:

  • it’s flexible since you can give special treatment to the editable label. You can change its color like I did here, put a context menu with actions like “Clear”, “Evaluate”, “Show Answer” etc.

  • It’s almost multiline. The flow layout panel takes care of the component wrapping and it will work if there are frequent breaks in the quiz string. Otherwise you will have a very big label that won’t fit in the panel. You can though use a trick to circumvent that and use \n to break long strings. You can handle \n in the SetQuizText() but I’ll leave that to you 🙂 Have in mind that id you don’t handle it the label will do and that won’t bind well with the FlowLayoutPanel.

  • TextBoxes can fit better. The editing text box that will fit 3 characters will not have the same with as the label with 3 characters. With this solution you don’t have to bother with that. Once the edited label is replaced by the text box, the next Controls will shift to the right to fit the text box. Once the label comes back, the other controls can realign.

What I don’t like though is that all these will come for a price: you have to manually align the controls. That’s why you see some magic numbers (which I don’t like and try hard to avoid them). Text box does not have the same height as the label. That’s why I’ve padded all labels 3 pixels on the top . Also for some reason that I don’t have time to investigate now, MeasureString() does not return the exact width, it’s tiny bit wider. With trial and error I realised that removing 4 pixels will better align the labels

Now you say there will be 300 strings so I guess you mean 300 “quizes”. If these are as small as the quick brown fox, I think the way multiline is handled in my solution will not cause you any trouble. But if the text will be bigger I would suggest you go with one of the other answers that work with multiline text boxes.

Have in mind though that if this grows more complex, like for example fancy indicators that the text was right or wrong or if you want the control to be responsive to size changes, then you will need text controls that are not provided by the framework. Windows forms library has unfortunately remained stagnant for several years now, and elegant solutions in problems such as yours are difficult the be found, at least without commercial controls.

Hope it helps you getting started.

I’ve worked up a bit easier solution to understand that might help you get started at the very least (I didn’t have time to play with multiple inputs in the same label, but I got it working correctly for 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: Forgot the to include the “Measure” method:

 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)); } } } 

Il risultato:

inserisci la descrizione dell'immagine qui

Then you should be able to assign event handlers to the individual text boxes/name them for easier access later on when the user interacts with the given input.

I would try something like this (sure will need some sizes adjustments):

  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); 
  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 and text Box for this, u can do it in any display it in any string control. Only u need user input position, string what u wanna change with symbols as place holder and input character, is sample on text box, at key input so u number of controls is not imported. For long string copy u can always for each char.