Как я (элегантно) переносить текстовое поле поверх метки в определенной части строки?
Я буду кормить несколько строк в ярлыки в 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));
Он будет выдавать этот результат:
Затем, после ввода пользователем значений, он будет выглядеть следующим образом:
И наконец, когда вы нажмете кнопку " Show Result
, она отобразит правильные ответы в зеленом цвете и неправильные ответы красного цвета:
Код
Вы можете скачать полный рабочий исходный код, например, здесь:
Реализация очень проста:
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
Определите, на какой символ щелкнули, если это был знак подчеркивания, затем размер подчеркивания слева и справа и показать текстовое поле поверх знака подчеркивания.
Вы можете настроить этот код, ярлык - это текстовое поле только для чтения, чтобы получить доступ к 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/...
Редактировать:
Я сделал это для многострочных текстовых полей:
Ответ 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 сбрасывает текст и показывает начальную маску.
Список слов инициализируется с указанием полного слова и количества смежных символов, которые нужно заменить маской: => 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)
}
}
Конечный результат будет выглядеть так:
Плюсы и минусы
Что я считаю хорошим с этим решением:
-
он гибкий, поскольку вы можете уделять особое внимание редактируемому ярлыку. Вы можете изменить свой цвет, как я здесь, поставить контекстное меню с такими действиями, как "Очистить", "Оценить", "Показать ответ" и т.д.
-
Он почти многострочный. Панель макета потока заботится об упаковке компонентов, и она будет работать, если в строке викторины будут частые перерывы. В противном случае у вас будет очень большая метка, которая не будет вписываться в панель. Вы можете использовать трюк, чтобы обойти это, и использовать \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));
}
}
}
Результат:
Затем вы должны иметь возможность назначать обработчики событий отдельным текстовым полям/имён для более легкого доступа позже, когда пользователь взаимодействует с данным вводом.
Ответ 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 всегда может быть для каждого символа.