ピクチャボックスを使って疑似的にボタンを作る
標準で用意されているボタンはフォーカスを奪ってしまいます。なにを言いたいかというと、矢印キーや TAB キーを積極的に使うGUI アプリケーションを作成したい場合、複数のボタンがあると、矢印キーでボタンのフォーカスが移動するだけです。TABキーも同様です。
これは Windows 標準の動作であって、何もおかしい動作ではないです。ただ、お客さんは、矢印キーでフォーカスが移動できるようにみたいなことを安易に注文してきます。PC-9801のソフトを作っているわけではないんですよ、と言いたいのはジッとがまん。
さて、私が考えた解決法は PictureBox を使ってボタンのようなものを作るという方法です。これならばフォーカスを奪うことはありません。Fig.1 に実例を示します。なかなかイイ線いっていると思いませんか。マウスオーバーやマウスダウンのときに色も変わります。以降に実例のサンプルコードを示します。PictureBoxPseudoButton というクラスを定義しました。
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;
}
}
}