Another quiz solution, using a class derived from TextBox a the editor for the missing letters.
What this code does:
1) Takes the Text of a Label control, a list of strings (words) and substrings of those words which are used as a mask to hide some of the letters in the words
2) Creates a mask of the substrings using two Unicode space chars (U+2007
and U+2002
) of different sizes, to match the size of the letters to substitute
3) Sizes up a border-less TextBox (the Editor
, a class object that inherits from Textbox
) using the calculated Width
and Height
(in pixels) of the substring. Sets the TextBox.MaxLength
property to the length of the substring.
4) Calculates the position of the substrings inside a Multiline Label Text, checks for duplicate patterns, and overlays the Texbox objects (Editor
class)
This method supports:
Proportional fonts. Only Unicode Fonts are supported.
The Labels' text can occupy multiple lines.
I've use a fixed-size font (Lucida Console) because of the mask char.
To deal with proportional fonts, two different mask chars are used, depending on the characters width
(i.e. different mask chars of different width to match the substituted characters width).
A visual representation of the results:
The TAB key is used to pass from a TextBox control to the next/previous.
The ENTER key is used to accept the edit. Then the code checks if it's a match.
The ESC key resets the text and shows the initial mask.

A list of words is initialized specifying a complete word and a number of contiguous characters to substitute with a mask: => jumped : umpe
and the associated Label control.
When a Quiz
class is initialized, it automatically subtitutes all words in the specified Label text with a TextBox mask.
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" } }));
This is the Quiz class:
It's job is to collect all the Editors (TextBoxes) that are used for each Label and calculate their Location, given the position of the string they have to substitute in each Label text.
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;
}
}
}
This is the Editor class (inherits from TextBox):
It builds and calculates the length of the mask chars and autosizes itself using this value.
Has basic editing capabilities.
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();
}
}