はじめに
まず、WinFormsが今更感ありますが、個人的には今でも利用していますのでご紹介となります。
標準のチェックボックスってフォントがデフォルトだったり小さいうちは気になりませんが、フォントサイズが大きい場合、こんな感じになります。
グリフ(チェックマークの部分)はサイズは指定が出来ませんし、自動的にサイズを合わせてくれるなんて事もありません。
また世の中にはグリフ部分をクリックしないといけないと思い込んでいるユーザーもいますので、そういったユーザーからは押しづらいとの不満の声が上がります。(.NETではプロパティで挙動を変えれますが、実際グリフ部分じゃないと応答しないタイプも見た事あります)
この様にWinForms標準コントロールの視覚効果に不満があった場合、解決策は概ね「独自描画 しかないし、めんどくさいし古いしxamlしか勝たん」になります。
個人的には見た目を良くするための描画処理を書くのは好きな方なので、苦にならないのですが、CheckBoxは難点があります。
それは、CheckAlignとTextAlignとAutoSizeの問題です。
CheckBoxはCheckAlignプロパティとTextAlignプロパティの値の組み合わせで、描画処理を工夫しないといけません。
しかし残念ながら、他の参考記事を見ていると、大体そこらへんは考慮されていませんし、同じようにAutoSizeも考慮されていない記事が多いです。
実際めんどくさいし、そのせいもあってか、標準のCheckBoxを継承しないで、Controlを派生して実装するのもあります。
さて今回紹介するのは、Alignも考慮し、AutoSizeも考慮したCheckBoxを継承したカスタム例です。(FlatStyleとか無視ですケド)
.NET 8.0での実装ですが.NET Freamwork系や Core系でもイケると思います。
標準コントロールと同じ位置関係になるよう、努力してみました。
サンプルコード
ちなみに長いです。また便宜上継承したクラスにひとまとめにしています。
環境
Windows 11 64bit
Visual Studio 2022
C# .NET 8.0 WinForms
public class CheckBoxEx : System.Windows.Forms.CheckBox
{
//Glyphのパス
private GraphicsPath _glyphPath;
//Glyphの背景のパス
private GraphicsPath _glyphBackPath;
//既定のAutoSizeを無効にし、独自のAutoSizeを実装する為に必要
private bool _autosize = true;
[DefaultValue(true)]
public override bool AutoSize
{
get => _autosize;
set
{
_autosize = value;
if (value)
{
AutoSizing();
}
}
}
//チェックマークを付けた時の色を任意で変更できるようにする
private SolidBrush _glyphBrush = new SolidBrush(Color.DodgerBlue);
/// <summary>
/// Glyphの色を設定または取得します。
/// </summary>
[DefaultValue(typeof(Color), "DodgerBlue")]
[Description("Glyphの色を設定または取得します。")]
public Color GlyphColor
{
get { return _glyphBrush.Color; }
set { _glyphBrush.Color = value; this.Refresh(); }
}
//Glyphのサイズを任意に変更できるようにする
private int _glyphSize = 16;
///<summary>Glyphのサイズを設定または取得します。</summary>
[DefaultValue(16)]
[Description("Glyphのサイズを設定または取得します。")]
public int GlyphSize
{
get => _glyphSize; set
{
_glyphSize = value;
if (_autosize)
{
AutoSizing();
}
}
}
//コンストラクタ
public CheckBoxEx()
{
//ちらつき防止の為にダブルバッファリングを有効にする
this.DoubleBuffered = true;
//チェックマークのパスを作成
_glyphPath = CreateCheckMark(16);
//背景のパスを作成
_glyphBackPath = CreateSquare(new RectangleF(0, 0, 16, 16));
}
//デザイナー上での表示を考慮する為にオーバーライド
protected override void OnInvalidated(InvalidateEventArgs e)
{
base.OnInvalidated(e);
if (_autosize && DesignMode && this.Appearance == Appearance.Normal)
{
AutoSizing();
}
}
//描画処理
protected override void OnPaint(PaintEventArgs pevent)
{
//AppearanceがNormalの時だけ独自の描画処理を行う
if (this.Appearance == Appearance.Normal)
{
CustomPaint(pevent);
}
else
{
base.OnPaint(pevent);
}
}
//テキストが変更された時にAutoSIzeが有効なら独自のサイズ変更処理を行う
protected override void OnTextChanged(EventArgs e)
{
base.OnTextChanged(e);
if (this.AutoSize && this.Appearance == Appearance.Normal)
{
AutoSizing();
}
}
//サイズが変更された時にAutoSIzeが有効なら独自のサイズ変更処理を行う
protected override void OnLayout(LayoutEventArgs levent)
{
base.OnLayout(levent);
if (this.AutoSize && this.Appearance == Appearance.Normal)
{
AutoSizing();
}
}
//独自の描画処理
private void CustomPaint(PaintEventArgs pevent)
{
//Glyphの描画領域を取得
Rectangle glyphRectangle = GetGlyphRectangle();
pevent.Graphics.Clear(this.BackColor);
//Glyphの描画位置を調整する為にTransformに行列を設定
pevent.Graphics.Transform = GetStretchMatrix(new Rectangle(0, 0, 16, 16), glyphRectangle);
if (this.Enabled == false)//Enabledがfalseの時はグレーで塗りつぶす
{
pevent.Graphics.FillPath(Brushes.Gray, _glyphBackPath);
}
else if (this.Checked)//チェックされている時はGlyphの色で塗りつぶす
{
pevent.Graphics.FillPath(_glyphBrush, _glyphBackPath);
}
else//チェックされていない時は白で塗りつぶす
{
pevent.Graphics.FillPath(Brushes.White, _glyphBackPath);
}
if (this.Checked)//チェックされている時はチェックマークを描画
{
pevent.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
pevent.Graphics.FillPath(Brushes.White, _glyphPath);
pevent.Graphics.SmoothingMode = SmoothingMode.Default;
}
//Transformをリセット
pevent.Graphics.ResetTransform();
//Glyphの背景の枠を描画するのにはみ出るため1px縮める※縮めないと右端と下端が見切れる
glyphRectangle.Width -= 1;
glyphRectangle.Height -= 1;
//Glyph背景の枠を描画
pevent.Graphics.DrawRectangle(Pens.Gray, glyphRectangle);
//テキストを描画する為のTextFormatFlagsを取得
TextFormatFlags f = GetTextFormatFlags();
if (this.Enabled == false)//Enabledがfalseの時はグレーで描画
{
TextRenderer.DrawText(pevent.Graphics, this.Text, this.Font, GetTextRectangle(), Color.Gray, f);
}
else//Enabledがtrueの時はForeColorで描画
{
TextRenderer.DrawText(pevent.Graphics, this.Text, this.Font, GetTextRectangle(), this.ForeColor, f);
}
}
//AutoSizeやTextAlignを考慮してTextFormatFlagsを取得する
private TextFormatFlags GetTextFormatFlags()
{
TextFormatFlags f = TextFormatFlags.Default;
if (_autosize)
{
switch (this.TextAlign)
{
case ContentAlignment.TopCenter:
f |= TextFormatFlags.HorizontalCenter;
break;
case ContentAlignment.TopRight:
f |= TextFormatFlags.Right;
break;
case ContentAlignment.MiddleLeft:
f |= TextFormatFlags.VerticalCenter;
break;
case ContentAlignment.MiddleCenter:
f |= TextFormatFlags.VerticalCenter | TextFormatFlags.HorizontalCenter;
break;
case ContentAlignment.MiddleRight:
f |= TextFormatFlags.VerticalCenter | TextFormatFlags.Right;
break;
case ContentAlignment.BottomCenter:
f |= TextFormatFlags.HorizontalCenter;
break;
case ContentAlignment.BottomRight:
f |= TextFormatFlags.Right;
break;
default:
break;
}
}
else
{
f = TextFormatFlags.WordBreak;
switch (this.TextAlign)
{
case ContentAlignment.TopLeft:
break;
case ContentAlignment.TopCenter:
f |= TextFormatFlags.HorizontalCenter;
break;
case ContentAlignment.TopRight:
f |= TextFormatFlags.Right;
break;
case ContentAlignment.MiddleLeft:
f |= TextFormatFlags.VerticalCenter;
break;
case ContentAlignment.MiddleCenter:
f |= TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter;
break;
case ContentAlignment.MiddleRight:
f |= TextFormatFlags.Right | TextFormatFlags.VerticalCenter;
break;
case ContentAlignment.BottomLeft:
f |= TextFormatFlags.Bottom;
break;
case ContentAlignment.BottomCenter:
f |= TextFormatFlags.Bottom | TextFormatFlags.HorizontalCenter;
break;
case ContentAlignment.BottomRight:
f |= TextFormatFlags.Bottom | TextFormatFlags.Right;
break;
default:
break;
}
}
return f;
}
//Glyphの描画領域を取得する
private Rectangle GetGlyphRectangle()
{
Rectangle r = new Rectangle(0, 0, _glyphSize, _glyphSize);
int halfWidth = (int)(this.Width / 2);
int halfHeight = (int)(this.Height / 2);
int halfGryph = (int)(_glyphSize / 2);
int middleX = halfWidth - halfGryph;
int middleY = halfHeight - halfGryph;
switch (this.CheckAlign)
{
case ContentAlignment.TopLeft:
break;
case ContentAlignment.TopCenter:
r.Offset(middleX, 0);
break;
case ContentAlignment.TopRight:
r.Offset(this.Width - _glyphSize, 0);
break;
case ContentAlignment.MiddleLeft:
r.Offset(0, middleY);
break;
case ContentAlignment.MiddleCenter:
r.Offset(middleX, middleY);
break;
case ContentAlignment.MiddleRight:
r.Offset(this.Width - _glyphSize, middleY);
break;
case ContentAlignment.BottomLeft:
r.Offset(0, this.Height - _glyphSize);
break;
case ContentAlignment.BottomCenter:
r.Offset(middleX, this.Height - _glyphSize);
break;
case ContentAlignment.BottomRight:
r.Offset(this.Width - _glyphSize, this.Height - _glyphSize);
break;
default:
break;
}
return r;
}
//テキストの描画領域を取得する
private Rectangle GetTextRectangle()
{
Rectangle r = Rectangle.Empty;
switch (this.CheckAlign)
{
case ContentAlignment.TopLeft:
r = new Rectangle(_glyphSize, 0, this.Width - _glyphSize, this.Height);
break;
case ContentAlignment.TopCenter:
r = new Rectangle(0, _glyphSize, this.Width, this.Height - _glyphSize);
break;
case ContentAlignment.TopRight:
r = new Rectangle(0, 0, this.Width - _glyphSize, this.Height);
break;
case ContentAlignment.MiddleLeft:
r = new Rectangle(_glyphSize, 0, this.Width - _glyphSize, this.Height);
break;
case ContentAlignment.MiddleCenter:
r = new Rectangle(0, 0, this.Width, this.Height);
break;
case ContentAlignment.MiddleRight:
r = new Rectangle(0, 0, this.Width - _glyphSize, this.Height);
break;
case ContentAlignment.BottomLeft:
r = new Rectangle(_glyphSize, 0, this.Width - _glyphSize, this.Height);
break;
case ContentAlignment.BottomCenter:
r = new Rectangle(0, 0, this.Width, this.Height - _glyphSize);
break;
case ContentAlignment.BottomRight:
r = new Rectangle(0, 0, this.Width - _glyphSize, this.Height);
break;
default:
break;
}
return r;
}
//AutoSizeが有効な時の独自のサイズ変更処理
private void AutoSizing()
{
SizeF textSize;
// テキストが空の場合
if (this.Text.Length == 0)
{
// Glyphのサイズに合わせる
this.Size = new Size(_glyphSize, _glyphSize);
return;
}
else
{
// テキストのサイズを取得
textSize = base.CreateGraphics().MeasureString(this.Text, this.Font);
}
// テキストのサイズとGlyphのサイズを比較して、大きい方に合わせる
int width;
int height;
switch (this.CheckAlign)
{
case ContentAlignment.TopCenter:
width = Math.Max(_glyphSize, (int)textSize.Width);
height = (int)textSize.Height + _glyphSize;
break;
case ContentAlignment.MiddleCenter:
width = Math.Max(_glyphSize, (int)textSize.Width);
height = Math.Max(_glyphSize, (int)textSize.Height);
break;
case ContentAlignment.BottomCenter:
width = Math.Max(_glyphSize, (int)textSize.Width);
height = (int)textSize.Height + _glyphSize;
break;
default:
width = (int)textSize.Width + _glyphSize;
height = Math.Max(_glyphSize, (int)textSize.Height);
break;
}
// サイズを設定
this.Size = new Size(width, height);
}
/// <summary>
/// チェックマークのパスを作成します。
/// </summary>
/// <param name="size"></param>
/// <returns></returns>
private GraphicsPath CreateCheckMark(float size)
{
float rate = 16 / size;
GraphicsPath path = new GraphicsPath();
path.AddLines(new[] { new PointF(12.58F * rate, 3.46F * rate), new PointF(7.06F * rate, 8.98F * rate), new PointF(3.42F * rate, 5.33F * rate), new PointF(1.64F * rate, 7.11F * rate), new PointF(5.29F * rate, 10.76F * rate), new PointF(7.06F * rate, 12.54F * rate), new PointF(8.84F * rate, 10.76F * rate), new PointF(14.36F * rate, 5.24F * rate) });
path.CloseAllFigures();
return path;
}
/// <summary>
/// 四角形のパスを作成します。
/// </summary>
/// <param name="rect"></param>
/// <returns></returns>
private GraphicsPath CreateSquare(RectangleF rect)
{
GraphicsPath path = new GraphicsPath();
path.AddRectangle(rect);
return path;
}
/// <summary>
/// 元矩形を目的の矩形にストレッチするための行列を取得します。
/// </summary>
/// <param name="sourceRect">元矩形を指定します</param>
/// <param name="destinationRect">目的の矩形を指定します</param>
/// <returns></returns>
private Matrix GetStretchMatrix(RectangleF sourceRect, RectangleF destinationRect)
{
float scaleX = destinationRect.Width / sourceRect.Width;
float scaleY = destinationRect.Height / sourceRect.Height;
Matrix stretchMatrix = new Matrix();
stretchMatrix.Translate(destinationRect.X - (sourceRect.X * scaleX), destinationRect.Y - (sourceRect.Y * scaleY));
stretchMatrix.Scale(scaleX, scaleY);
return stretchMatrix;
}
}
解説(?)
長いので解説しません。すいません。
必要そうな部分はコメント書いてあるので読んでください。
コツみたいな所は、やっぱりグリフの位置算出とその影響をうけたテキスト描画の位置算出とか、自動サイズ変更処理の所でしょうか。
まぁこんな感じでイケますという例です。
あとがき
ここまで書かないといけないなら、諦めた方がよい
さて、頑張るとこんな感じなりますが、ここまでやったらRadioButtonも対応したくなりますよね?(圧)
実はこのコードまんま継承するクラスをRadioButtonに変更するだけでイケるクールな実装例です。
そんな事を考えると、実装は共通化したくなります。
今回はチェックマークのみですが、たとえばサークルだったり、クロスマークだったり、色々とグリフを変更できると面白いですよね。例えばグリフはプロパティにして任意に変えれたりとか、状態に合わせてもっと柔軟に視覚表現をいじれるような感じとか、アイディアは色々あると思います。
ただし、そこまでやる場合はインターフェイス実装して処理を共通化した方が良いですというか、推奨します。
現場にもよると思いますが、後の為にもおススメします。
今度時間ある時に僕が普段使っている色々やったったモノを紹介できればと思いますが、今回はここまで。
皆様の参考になれば幸いです。