Comment puis-je (élégamment) transposer textbox sur l'étiquette à une partie spécifique de la chaîne?

je vais alimenter un certain nombre de chaînes dans des étiquettes sur un formulaire Windows (Je ne les utilise pas beaucoup). Les cordes vont être semblable à la suivante:

"The quick brown fox j___ed sur le l__y hound"

je veux afficher la chaîne dans une étiquette mais superposer une boîte de texte exactement où sont les lettres manquantes.

il y aura plus de 300 cordes, et je cherche la façon la plus simple, la plus élégante de le faire.

comment repositionner le zone de texte avec précision pour chaque chaîne?

EDIT: une MaskTextBox ne fonctionnera pas car j'ai besoin d'un support multiligne.

20
demandé sur Dhaval Asodariya 2018-01-23 15:31:29

10 réponses

pour satisfaire à cette exigence, IMO il est préférable d'utiliser les caractéristiques des formes Windows qui permettent l'interopérabilité avec HTML ou WPF et d'Accueil WebBrowser contrôle ou WPF ElementHost pour montrer le contenu aux utilisateurs. Avant de lire cette réponse, veuillez considérer:

  • les utilisateurs ne devraient pas pouvoir effacer le ____ champs. S'ils peuvent les effacer, une fois qu'ils sont passés à un autre vide, ils perdront la capacité de trouver le champ nettoyé.
  • il vaut mieux permettre aux utilisateurs d'utiliser Onglet pour passer entre ____ champs.
  • Comme il est mentionné dans la question: une MaskTextBox ne fonctionnera pas car j'ai besoin d'un support multiligne.
  • Comme il est mentionné dans la question: il y aura plus de 300 cordes donc mélanger beaucoup de Windows Forms control n'est pas une bonne idée.

utilisant Html comme vue d'un modèle C# et l'afficher dans le contrôle WebBrowser

Ici Je vais partager une réponse simple basé sur montrant HTML WebBrowser de contrôle. En option vous pouvez utiliser un WebBrowser contrôler et créer du html approprié à afficher dans WebBrowser contrôle à l'aide d'une classe de mode.

l'idée principale est de créer une sortie html basée sur le modèle quiz (y compris le texte original et les ragnes de blancs) et de rendre le modèle en utilisant html et en le montrant dans un WebBrowser de contrôle.

Par exemple utiliser le modèle suivant:

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

Il va rendre cette sortie:

fill in the blank - initial

puis après les valeurs saisies par l'utilisateur, il affichera de cette façon:

fill in the blank - having answers

et enfin, quand vous cliquez sur Show Result bouton, il affiche les réponses correctes dans la couleur verte, et de mauvaises réponses en couleur rouge:

fill in the blank - showing results

Code

vous pouvez télécharger le code source complet par exemple ici:

La mise en œuvre est assez simple:

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*/
    }
}

voici le code complet du Quiz catégorie:

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 = $@"<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; }
}

Et voici le contenu du gabarit 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>
4
répondu Reza Aghaei 2018-02-04 19:49:54

une option est d'utiliser une boîte de texte masquée.

Dans votre exemple, vous réglez le masque de:

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

qui apparaîtrait comme:

"The quick brown fox j___ed over the lazy hound"

Et permettent seulement de 3 caractères (a-z et A-Z) à être entré dans l'espace. Et le masque peut être facilement changé via le code.

modifier: Pour plus de commodité...

Voici une liste et une description des caractères de masquage

(prise de 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.

tous les autres personnages - littéraux. Tous les éléments non masqués apparaîtront comme eux-mêmes dans MaskedTextBox. Les lettres occupent toujours une position statique dans le masque au moment de l'exécution, et ne peuvent pas être déplacées ou supprimées par l'utilisateur.

16
répondu Gravitate 2018-01-23 12:57:17

enter image description here

déterminez sur quel caractère vous avez cliqué, si c'était un underscore, puis faites la taille des underscores gauche et droite et montrez une boîte de texte au dessus des underscores.

vous pouvez modifier ce code, le label est en fait une boîte de texte en lecture seule pour avoir accès au GetCharIndexFromPosition et GetPositionFromCharIndex méthodes.

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

pour désactiver la boîte de texte (fausse étiquette) de la sélection: https://stackoverflow.com/a/42391380/495455

Edit:

j'ai fait le travail pour les zones de texte multiligne:

enter image description here

5
répondu Jeremy Thompson 2018-02-03 07:43:33

cela peut être exagéré en fonction de la complexité que vous voulez que ce soit, mais un contrôle de navigateur Web winforms (qui est essentiellement MSIE tournant à l'intérieur de votre application Winforms) peut fonctionner comme un éditeur où vous contrôlez quelles pièces sont modifiables.

Chargez votre contenu avec les parties modifiables étiquetées comme telles, par exemple:

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

une autre option, Un peu plus de travail à coder mais plus légère en termes de mémoire / ressources, serait d'utiliser un FlowLayoutPanel, ajouter des panneaux normaux au panneau FlowLayoutPanel, puis mettre des étiquettes ou des boîtes de texte sur ces panneaux selon si un panneau représente une partie fixe ou modifiable, et les redimensionner pour correspondre à la longueur du contenu. Vous pouvez utiliser MeasureString pour connaître la largeur du contenu dans chaque étiquette/boîte de texte à des fins de redimensionnement.

4
répondu KristoferA 2018-01-31 21:55:46

un autre, en utilisant une simple commande TextBox.

EDIT1: Ajout du support pour les polices proportionnelles. Seules les polices Unicode sont supportées.

EDIT2: a abandonné le paramètre de soulignement, en utilisant maintenant 2 caractères unicode et le soulignement de police. Ajout du support IDisposable.

EDIT3: ajout d'un support multiligne

Quel est ce code:

1) prend une liste de chaînes (mots) et sous-chaînes de ceux mots et le texte d'un contrôle D'étiquette

2) crée un masque des substrats en utilisant deux caractères D'Espace Unicode (U+2007 et U+2002) de taille différente, pour correspondre à la taille des lettres à remplacer

3) Taille vers le haut D'une zone de texte sans bordure (un objet de classe qui hérite de la zone de texte) en utilisant la largeur et la hauteur calculées (en pixels) du substrat. Définit sa propriété MaxLength à la longueur de la substring.

4) calcule la position des sous-couches à l'intérieur d'un texte D'étiquette multiligne, vérifie les motifs dupliqués, et superpose les objets Texbox (Classe Editor)

j'ai utilisé une police de taille fixe (Console Lucida) à cause du masque char.

Pour gérer les polices proportionnelles, deux caractères de masque différents sont utilisés, en fonction de la largeur des caractères

(c'est-à-dire différentes barres de masque de largeur différente pour correspondre à la largeur des caractères substitués).

Une représentation visuelle des résultats:

La touche TAB est utilisée pour passer de la commande TextBox à la commande next/previous.

La touche entrée est utilisée pour accepter l'édition. Alors le code vérifie s'il correspond.

La touche ESC réinitialise le texte et affiche le masque initial.

enter image description here

Une liste de mots est initialisé spécifier un mot complet et un nombre de caractères contigus à remplacer par un masque:=> jumped : umpe

et le contrôle Label.

Lorsqu'un Quiz la classe est initialisée, elle substitue automatiquement tous les mots dans le texte D'étiquette spécifié avec un masque de 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" } }));

C'est le Quiz de la classe:

Son travail est de collecter tous les éditeurs (boîtes de textes) qui sont utilisés pour chaque étiquette et de calculer leur emplacement, compte tenu de la position de la chaîne qu'ils doivent remplacer dans chaque texte D'étiquette.

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

c'est la classe Editor (hérite de TextBox):

Il construit et calcule la longueur des barres de masque et se positionne lui-même en utilisant cette valeur.

possède des capacités d'édition de 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();
    }
}
3
répondu Jimi 2018-02-10 15:40:58

envisagez d'utiliser une combinaison de DataGridView et d'une colonne de cellules masquées.

sur L'affichage de contrôle D'édition, vous changeriez le masque de cette ligne particulière.

voici un exemple d'utilisation du code qui inclut la grille et le masquage unique pour chaque ligne.

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

et la colonne de grille, provenant d'ici: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
2
répondu Ctznkane525 2018-02-02 12:43:02

Voici comment je l'aborderais. Séparez avec l'expression régulière la chaîne et créez des étiquettes séparées pour chacune des sous-chaînes. Mettre toutes les étiquettes dans un FlowLayoutPanel. Lorsqu'une étiquette est cliqué, l'enlever et sur la même position ajouter la zone d'édition. Lorsque le focus est perdu (ou enter est pressé), supprimez la zone de texte et remettez l'étiquette en place; Réglez le texte de l'étiquette sur le texte de la zone de texte.

tout d'Abord créer custom UserControl comme la suivant

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

celui-ci accepte le texte du quiz puis le divise avec regex et crée les étiquettes et les zones de texte. Si vous êtes intéressé pour savoir comment faire le retour Regex les allumettes et non seulement les substrings ont un look ici

alors pour prendre soin de la transition entre l'édition, j'ai créé un EditableLabel classe. Il ressemble à ceci

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

vous pouvez utiliser le custom UserControl en le faisant glisser et le laisser tomber de la concepteur (après que vous générez avec succès) ou de l'ajouter comme ceci:

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

Le résultat final ressemblera à ceci:

Word quiz form

pour et contre

Ce que je considère bon avec cette solution:

  • c'est flexible puisque vous pouvez donner un traitement spécial à l'étiquette modifiable. Vous pouvez changer sa couleur Comme je l'ai fait ici, mettre un menu contextuel avec des actions comme "clair", "évaluer", " afficher la réponse" etc.

  • presque multiligne. Le panneau de configuration de flux prend soin de l'emballage du composant et il fonctionnera si il y a des pauses fréquentes dans la chaîne du quiz. Sinon, vous aurez une très grande étiquette qui ne tiendra pas dans le panneau. Vous pouvez cependant utiliser un truc pour contourner cela et utiliser \n pour briser de longues cordes. Vous pouvez gérer \n dans le SetQuizText() mais je vous laisse cela :) ayez à l'esprit que si vous ne vous en occupez pas le label fera l'affaire et cela ne se liera pas bien avec le FlowLayoutPanel.

  • les boîtes à textes conviennent mieux. La zone d'édition de texte qui s'adaptera à 3 caractères n'aura pas la même chose que l'étiquette avec 3 caractères. Avec cette solution, vous n'avez pas à s'embêter avec ça. Une fois que l'étiquette éditée est remplacée par la zone de texte, les contrôles suivants se déplacent vers la droite pour s'adapter à la zone de texte. Une fois que l'étiquette revient, les autres contrôles peuvent réaligner.

mais ce que je n'aime pas, c'est que tout cela aura un prix: il faut aligner manuellement les commandes. C'est pourquoi vous voyez des nombres magiques (que je n'aime pas et j'essaie de les éviter). Zone de texte n'ont pas la même hauteur que l'étiquette. C'est pourquoi j'ai rembourré toutes les étiquettes 3 pixels en haut. Aussi pour une raison que je n'ai pas le temps d'étudier maintenant, MeasureString() ne renvoie pas la largeur exacte, elle est légèrement plus large. Avec d'essai et d'erreur, j'ai réalisé que Supprimer 4 pixels permettra de mieux aligner les étiquettes

Maintenant vous dites qu'il y aura 300 cordes donc je suppose que vous voulez dire 300 "quizes". Si ceux-ci sont aussi petits que le renard brun rapide, je pense que la façon multiligne est manipulée dans ma solution ne vous causera aucun problème. Mais si le texte sera plus grand, je vous suggère d'aller avec l'un des autres réponses que travailler avec du texte multiligne boîtes.

ayez à l'esprit cependant que si cela se développe plus complexe, comme par exemple des indicateurs fantaisistes que le texte était bon ou mauvais ou si vous voulez que le contrôle soit responsive pour les changements de taille, alors vous aurez besoin de contrôles texte qui ne sont pas fournis par le framework. Windows forms library est malheureusement resté stagnant depuis plusieurs années maintenant, et des solutions élégantes dans des problèmes tels que le vôtre sont difficiles à trouver, au moins sans contrôles commerciaux.

J'espère que cela vous aidera à vous lancer.

2
répondu Stelios Adamantidis 2018-02-04 00:10:38

j'ai trouvé une solution un peu plus facile à comprendre qui pourrait vous aider à démarrer à tout le moins (je n'ai pas eu le temps de jouer avec plusieurs entrées dans le même label, mais je l'ai fait fonctionner correctement pour 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: oublié D'inclure la méthode" Measure":

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

Le résultat:

enter image description here

Alors vous devriez être en mesure d'attribuer des gestionnaires d'événements pour l'individu zones de texte/nom pour un accès plus facile plus tard, lorsque l'utilisateur interagit avec l'entrée donnée.

1
répondu Dr Archer 2018-02-04 01:44:45

je voudrais essayer quelque chose comme ceci (vous aurez besoin de certaines tailles ajustements):

    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);
1
répondu jason 2018-02-04 20:53:36
    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 N'a pas besoin D'étiquette et de zone de texte pour cela, u peut le faire dans n'importe quel affichage il dans n'importe quel contrôle de chaîne. Seul u a besoin de la position d'entrée de l'utilisateur, chaîne ce que u veut changer avec des symboles comme le support de place et le caractère d'entrée, est l'échantillon sur la boîte de texte, à l'entrée de clé afin u nombre de contrôles ne sont pas importés. Pour la copie longue chaîne u peut toujours pour chaque char.

1
répondu Norbert Ziemniak 2018-02-05 15:45:27