0

Imagine the following construction in WinForms .NET. A WinForms form contains a custom control with several buttons, which are instances of the traditional Button class. One of these buttons is the default button for the form. The custom control executes the action associated with the default button when ENTER is pressed. This is done in the redefined ProcessCmdKey method:

protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
    if (keyData == Keys.Return)
    {
        buttonOK_Click(null, EventArgs.Empty);
        return true;
    }
    return base.ProcessCmdKey(ref msg, keyData);
}

The default button must have an additional visual cue telling the user that this is the default button (an extra border inside the button). If we did this in a normal form, we would set its AcceptButton property. However, this approach is not applicable here. Even if we find the parent form using the Control.FindForm method or with an expression like (this.Parent as Form), we cannot set the AcceptButton property of the host form and then clear it the right way without resource leak or similar problems (a lot of technical details to place here and to bloat the question).

The first possible way to solve this task is to redefine or enhance the drawing of the button. Is there a relatively easy way to draw a button as the default button with the corresponding visual cue without implementing full custom painting? In my understanding, we might write a special class for our default button based on the following core:

internal class DefaultButton : Button
{
    protected override void OnPaint(PaintEventArgs pevent)
    {
        Rectangle rc = new Rectangle(0, 0, this.Width, this.Height);
        ButtonRenderer.DrawButton(pevent.Graphics, rc, System.Windows.Forms.VisualStyles.PushButtonState.Default);
    }
}

However, it should take into account the focused state, whether another button on a form is focused (in this case the default button is not drawn with the visual cue), and the like. I could not find a good example of this to use as a basis for my development.

Another possible way to solve my problem could be setting the protected IsDefault property or/and specifying the BS_DEFPUSHBUTTON flag in the overridden CreateParams method in a class inherited from the Button class, for example:

internal class DefaultButton : Button
{
    public DefaultButton() : base()
    {
        IsDefault = true;
    }

    protected override CreateParams CreateParams
    {
        get
        {
            const int BS_DEFPUSHBUTTON = 1;
            CreateParams cp = base.CreateParams;
            cp.Style |= BS_DEFPUSHBUTTON;
            return cp;
        }
    }
}

But I could not make this code work. Buttons based on this class are always drawn as normal push buttons without the default button visual cue.

TecMan
  • 2,743
  • 2
  • 30
  • 64
  • 1
    "the problem is that the custom control does not have access to the parent form to set its AcceptButton property" - Not true! See the [Control.FindForm Method](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.control.findform?view=windowsdesktop-6.0) – TnTinMn Sep 14 '22 at 14:10
  • Do you mean drawing the focus rectangle regardless whether the `Button` is focused or not? If so, you can handle that user control Button `Paint` event to draw the focus rectangle **if** it is not focused. Otherwise, the default paint will take care of that. – dr.null Sep 14 '22 at 22:26
  • @dr.null, I can get a reference to the parent form object with `Control.FindForm` or a simpler `this.Parent as Form` expression, but I can't use this form's AcceptButton property because of other technical restrictions of this project. I have just updated the question accordingly. – TecMan Sep 19 '22 at 10:37
  • It's not quite clear what is the concern about FindForm. [Container controls use FindForm to update DefaultButton of a Form](https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/ContainerControl.cs,588). – Reza Aghaei Sep 19 '22 at 14:00
  • @RezaAghaei, even if I have a reference to the parent form, the infrastructure of the solution does not allow to correctly set/clear its AcceptButton property. The counterquestion for you: how to use the `Form.UpdateDefaultButton` method you suggested if it is a protected method we can't override because my custom control works with the host it can't change? – TecMan Sep 19 '22 at 14:10
  • I'm not suggesting you to use `Form.UpdateDefaultButton`, I'm just telling, finding the host form and calling it's method is an allowed operation even inside the Windows Forms framework itself. I can share an example, which may help you to get closer to the solution; however, in general I'm not sure about the original requirement; and I have questions like why a user control should set the DefaultButton; or what if you have multiple instances of such user controls on the form, etc. – Reza Aghaei Sep 19 '22 at 14:25

1 Answers1

0

I'm not sure about the original requirement; for example I don't have any idea why a UserControl itself should set the AcceptButton of a Form, or what is the expected behavior if there are multiple instances of such controls on the form. It doesn't seem to be responsibility of the UserControl to set the AcceptButton of the Form and there might be better solutions, like relying on events and setting the AcceptButton.

Anyways, the following code example shows you how to set the AcceptButton of a Form; maybe it helps you to find a solutions. The highlights of the code:

  • The code uses dispose to set the AcceptButton to null.
  • The code implements ISupportInitialize to set the accept button after initialization of the control is done. If you create the control instance at run-time with code, don't forget to call EndInit, like this: ((System.ComponentModel.ISupportInitialize)(userControl11)).EndInit(); after adding it to the Form, but if you use designer, the designer will take care of that.
  • The code calls NotifyDefault(true) just for visual effect in design time when it's hosted on a form.

Here's the example:

using System;
using System.ComponentModel;
using System.Windows.Forms;
namespace WindowsFormsApp1
{
    public class UserControl1 : UserControl, ISupportInitialize
    {
        /// <summary> 
        /// Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        #region Component Designer generated code

        /// <summary> 
        /// Required method for Designer support - do not modify 
        /// the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            this.button1 = new System.Windows.Forms.Button();
            this.button2 = new System.Windows.Forms.Button();
            this.textBox1 = new System.Windows.Forms.TextBox();
            this.SuspendLayout();
            // 
            // button1
            // 
            this.button1.Location = new System.Drawing.Point(15, 57);
            this.button1.Name = "button1";
            this.button1.Size = new System.Drawing.Size(75, 23);
            this.button1.TabIndex = 0;
            this.button1.Text = "button1";
            this.button1.UseVisualStyleBackColor = true;
            this.button1.Click += new System.EventHandler(this.button1_Click);
            // 
            // button2
            // 
            this.button2.Location = new System.Drawing.Point(96, 57);
            this.button2.Name = "button2";
            this.button2.Size = new System.Drawing.Size(75, 23);
            this.button2.TabIndex = 1;
            this.button2.Text = "button2";
            this.button2.UseVisualStyleBackColor = true;
            this.button2.Click += new System.EventHandler(this.button2_Click);
            // 
            // textBox1
            // 
            this.textBox1.Location = new System.Drawing.Point(15, 17);
            this.textBox1.Name = "textBox1";
            this.textBox1.Size = new System.Drawing.Size(100, 20);
            this.textBox1.TabIndex = 2;
            // 
            // UserControl1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.Controls.Add(this.textBox1);
            this.Controls.Add(this.button2);
            this.Controls.Add(this.button1);
            this.Name = "UserControl1";
            this.Size = new System.Drawing.Size(236, 106);
            this.ResumeLayout(false);
            this.PerformLayout();

        }

        #endregion
        private System.Windows.Forms.TextBox textBox1;
        public System.Windows.Forms.Button button1;
        public System.Windows.Forms.Button button2;
        public UserControl1()
        {
            InitializeComponent();
            //Just for visual effect in design time when it's hosted on a form
            button2.NotifyDefault(true); 
        }
        private void button1_Click(object sender, EventArgs e)
        {
            MessageBox.Show("1");
        }
        private void button2_Click(object sender, EventArgs e)
        {
            MessageBox.Show("2");
        }
        public void BeginInit()
        {
        }
        public void EndInit()
        {
            var f = this.FindForm();
            if (f != null)
                f.AcceptButton = button2;
        }
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            if (disposing)
            {
                var f = this.FindForm();
                if (f != null)
                    f.AcceptButton = null;
            }
            base.Dispose(disposing);
        }
    }
}
Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • The whole construction is the following. There is a main form from which a dialog can be displayed. The dialog form is a container for custom controls like mine, and these custom controls are added to the dialog form dynamically before the dialog appears. These custom controls can provide the OK/Cancel buttons, and if the OK button is available, it must be set as the default button for the dialog form. – TecMan Sep 19 '22 at 15:49
  • Then the right approach is having the OK and Cancel button in the dialog, OK should be the AcceptButton, and in Click event handler, it should call a method of the UserControl (which could be done by implementing an interface). – Reza Aghaei Sep 19 '22 at 17:03
  • Another approach is exposing an AcceptButton property for the UserControl and just assign it to the AcceptButton of the form. – Reza Aghaei Sep 19 '22 at 17:07
  • In this solution the host dialog CANNOT BE CHANGED (re-coded, re-compiled). That's the main problem. The dialog just provides a site for hosting custom controls. – TecMan Sep 20 '22 at 06:54
  • It doesn't matter; The solution is creating a UserControl which implement a specific interface, or has an AcceptButton, and then assign the accept button of the control to accept button of dialog. – Reza Aghaei Sep 20 '22 at 07:03
  • You also have another solution that I shared in the post, with sample code. – Reza Aghaei Sep 20 '22 at 07:03