ピクチャボックスを使って疑似的にボタンを作る

標準で用意されているボタンはフォーカスを奪ってしまいます。なにを言いたいかというと、矢印キーや TAB キーを積極的に使うGUI アプリケーションを作成したい場合、複数のボタンがあると、矢印キーでボタンのフォーカスが移動するだけです。TABキーも同様です。

これは Windows 標準の動作であって、何もおかしい動作ではないです。ただ、お客さんは、矢印キーでフォーカスが移動できるようにみたいなことを安易に注文してきます。PC-9801のソフトを作っているわけではないんですよ、と言いたいのはジッとがまん。

さて、私が考えた解決法は PictureBox を使ってボタンのようなものを作るという方法です。これならばフォーカスを奪うことはありません。Fig.1 に実例を示します。なかなかイイ線いっていると思いませんか。マウスオーバーやマウスダウンのときに色も変わります。以降に実例のサンプルコードを示します。PictureBoxPseudoButton というクラスを定義しました。

FIg. 1 ピクチャボックスで疑似ボタンを作る
using System;
using System.Drawing;
using System.Windows.Forms;

namespace aaa
{
	public partial class Form1 : Form
	{

		PictureBoxPseudoButton buttonA;
		PictureBoxPseudoButton buttonB;
		PictureBoxPseudoButton buttonC;

		public Form1()
		{
			InitializeComponent();
		}

		private void Form1_Load( object sender, EventArgs e )
		{

			const String FONT_NAME = "MS UI Gothic";
			const float FONT_SIZE = 12.0f;

			// ピクチャボックスによる疑似ボタンを作成する.
			buttonA = new PictureBoxPseudoButton( "push A", FONT_NAME, FONT_SIZE );
			buttonB = new PictureBoxPseudoButton( "push B", FONT_NAME, FONT_SIZE );
			buttonC = new PictureBoxPseudoButton( "push C", FONT_NAME, FONT_SIZE );

			// 名前をつけておくと何かと便利.
			buttonA.Name = "buttonA";
			buttonB.Name = "buttonB";
			buttonC.Name = "buttonC";

			// イベントハンドラを登録する.
			buttonA.Click += buttonX_Click;
			buttonB.Click += buttonX_Click;
			buttonC.Click += buttonX_Click;

			// ボタンのサイズを決める.
			buttonA.Size = new Size( 128, 128 );
			buttonB.Size = new Size( 128, 128 );
			buttonC.Size = new Size( 128, 128 );

			// これを忘れると画面に現れない.
			this.Controls.Add( buttonA );
			this.Controls.Add( buttonB );
			this.Controls.Add( buttonC );

			// 配置場所を決める(この例ではウインドウ左上隅に水平に並ぶ).
			int MARGIN_XY = 8;
			buttonA.Location = new Point(                 MARGIN_XY, MARGIN_XY );
			buttonB.Location = new Point( buttonA.Right + MARGIN_XY, MARGIN_XY );
			buttonC.Location = new Point( buttonB.Right + MARGIN_XY, MARGIN_XY );

		}

		private void buttonX_Click( object sender, EventArgs e )
		{

			String str;

			if      ( sender == buttonA ){ str = $"you puch {buttonA.Name}"; }
			else if ( sender == buttonB ){ str = $"you puch {buttonB.Name}"; }
			else if ( sender == buttonC ){ str = $"you puch {buttonC.Name}"; }
			else                         { return; }

			this.Text = str;

		}

	}
}

こちらが定義したクラスです。とりあえず Windows11 のデフォルトっぽい色にしてありますが、いろいろいじれば変更できます。

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

namespace aaa
{

	public class PictureBoxPseudoButton : UserControl
	{

		private Color ColorBackground = SystemColors.Control;
		private string Caption = "button";

		private string FontName;
		private float FontSize;

        private Color ColorNormallyState;  // 通常時の色.
        private Color ColorMouseOverState; // マウスオーバー時の色.
        private Color ColorMouseDownState; // マウスダウン時の色.

		private Color ColorButtonFrame; // ボタンの縁取りの色.

		public PictureBoxPseudoButton( String text, String font_name, float font_size )
		{

			this.DoubleBuffered = true;

			// デフォルトのサイズ.
			this.Size = new Size( 100, 40 );

			// コンストラクタ引数.
			Caption = text;
			FontName = font_name;
			FontSize = font_size;

			// 通常時.
			ColorNormallyState = Color.FromArgb( 242, 242, 242 );

			// マウスオーバー時 (淡いブルー).
			ColorMouseOverState = Color.FromArgb( 230, 240, 255 );

			// マウスダウン時 (マウスオーバーより少し濃い).
			ColorMouseDownState = Color.FromArgb( 200, 220, 255 );

			// ボタンの縁取り.
			ColorButtonFrame = Color.FromArgb( 0, 120, 215 );

		}

		protected override void OnPaint( PaintEventArgs e )
		{

			base.OnPaint( e );

			Graphics grph = e.Graphics;
			grph.SmoothingMode = SmoothingMode.AntiAlias;

			int pw = this.ClientRectangle.Width;
			int ph = this.ClientRectangle.Height;

			// 四隅が丸い矩形のパスを作る.
			const int CORNER_RADIUS = 4;
			Rectangle rct = new Rectangle( 0, 0, pw - 1, ph - 1 );
			using ( GraphicsPath path = CreateRoundedRectanglePath( rct, CORNER_RADIUS ) )
			{

				// パスの内側を塗りつぶす.
				using ( SolidBrush sb = new SolidBrush( ColorBackground ) )
				{
					e.Graphics.FillPath( sb, path );
				}

				// パスに沿って線を引く.
				using ( Pen pen = new Pen( ColorButtonFrame, 1.0f ) )
				{
					grph.DrawPath( pen, path );
				}

			}

			// ボタンの真ん中に文字列を描画する.
			using ( Font font = new Font( FontName, FontSize, FontStyle.Regular ) )
			{

				SizeF txt_sz = grph.MeasureString( Caption, font );
				float txt_out_x = ( pw - txt_sz.Width ) / 2.0f;
				float txt_out_y = ( ph - txt_sz.Height ) / 2.0f;

				using ( Brush sb_txt = new SolidBrush( SystemColors.ControlText ) )
				{
					grph.DrawString( Caption, font, sb_txt, txt_out_x, txt_out_y );
				}

			}
		}

		protected override void OnMouseEnter( EventArgs e )
		{

			base.OnMouseEnter( e );

			// マウスオーバー時の色
			ColorBackground = ColorMouseOverState;
			this.Invalidate();

		}

		protected override void OnMouseLeave( EventArgs e )
		{

			base.OnMouseLeave( e );

			// 通常時の色
			ColorBackground = ColorNormallyState;
			this.Invalidate();

		}

		protected override void OnMouseDown( MouseEventArgs e )
		{

			base.OnMouseDown( e );

			// クリック時の強調色
			ColorBackground = ColorMouseDownState;
			this.Invalidate();

		}

		protected override void OnMouseUp( MouseEventArgs e )
		{

			base.OnMouseUp( e );

			// マウスアップ時に通常の色に戻す
			ColorBackground = ColorNormallyState;
			this.Invalidate();

		}

		private GraphicsPath CreateRoundedRectanglePath( Rectangle rect, int corner_radius )
		{

			GraphicsPath path = new GraphicsPath();

			int r = corner_radius;
			int dia = r * 2;

			// 円弧の回転方向は時計回りが正.
			const int DEG_SWEEP = 90;
			int deg_beg;

			// 左上隅から右に向かって線を引く.
			deg_beg = 180;
			path.AddArc( rect.X, rect.Y, dia, dia, deg_beg, DEG_SWEEP );
			path.AddLine( rect.X + r, rect.Y, rect.Right - r, rect.Y );

			// 右上隅から下に向かって線を引く.
			deg_beg = 270;
			path.AddArc( rect.Right - dia, rect.Y, dia, dia, deg_beg, DEG_SWEEP );
			path.AddLine( rect.Right, rect.Y + r, rect.Right, rect.Bottom - r );

			// 右下隅から左に向かって線を引く.
			deg_beg = 0;
			path.AddArc( rect.Right - dia, rect.Bottom - dia, dia, dia, deg_beg, DEG_SWEEP );
			path.AddLine( rect.Right - r, rect.Bottom, rect.X + r, rect.Bottom );

			// 左下隅から上に向かって線を引く.
			deg_beg = 90;
			path.AddArc( rect.X, rect.Bottom - dia, dia, dia, deg_beg, DEG_SWEEP );
			path.AddLine( rect.X, rect.Bottom - r, rect.X, rect.Y + r );

			// パスを閉じる.
			path.CloseFigure();

			// パスを返す.
			return path;

		}

		public void SetText( String text )
		{
			Caption = text;
		}

	}
}