Windows Forms の標準 ProgressBar だけでは「何%まで進んだか」を文字で分かりやすく表示できず、ユーザーにとって進捗状況が直感的に伝わりにくくなりがちです。本記事では VB.NET のカスタムコントロール ProgressBarZ を題材に、進捗バーの上に任意のテキストを動的に重ねて描画する実装を、プロパティ設計から描画処理、チラつき対策まで丁寧に解説します。
ProgressBar にテキストを重ねて表示したい典型的なシナリオ
Windows Forms アプリの開発では、標準の ProgressBar をそのまま使うと「バーが伸びているだけ」で、ユーザーに伝わる情報は次のように限定されます。
- 現在の進捗率がわからない(50%なのか 80%なのか曖昧)
- 何を処理しているのかが伝わりにくい
- マルチステップ処理(手順 1/3 など)が表現しづらい
そこでよく行われるのが、進捗バーの上に「50%」「ファイルコピー中…」といった文字列を重ねて描画する方法です。
しかし、単純にカスタムクラスに固定文字列を書いてしまうと、次のような問題が起こります。
- 文字列がハードコーディングされていて変更できない
- 外部(フォーム側)から動的にテキストを設定できない
- 再描画が正しく行われず、テキストが更新されないことがある
この記事では、これらの問題を解決するために、
- 公開プロパティ
DisplayTextを追加して外からテキストを設定できるようにする - プロパティ変更時に
Invalidate()を呼び出して再描画をトリガーする - 追加の描画メソッド(ここでは
AdditionalPaint)でプロパティ値を描画する
という流れで、実用的な ProgressBar 拡張クラス ProgressBarZ を組み上げていきます。
完成イメージと設計方針
まず、目指す完成イメージと設計方針を整理しておきます。
ProgressBarを継承したクラスProgressBarZを作成する- 標準のバー描画(緑のバー)はそのまま利用し、その上に文字列だけ重ね描きする
- 文字列は
DisplayTextプロパティで外部から自由に設定できる - 文字列変更時は自動的に再描画され、リアルタイムに更新される
- フォントや色は、コントロールの
Font/ForeColorを利用する
この構成にすることで、フォーム側からは次のような、とてもシンプルな書き方で利用できます。
ProgressBarZ1.Value = 50
ProgressBarZ1.DisplayText = "50 % 完了"
「プロパティを変更するだけで描画が更新される」状態にするのがポイントです。
DisplayText プロパティを追加する
まずは文字列を保持するためのフィールドと、公開プロパティ DisplayText を追加します。
Public Class ProgressBarZ
Inherits ProgressBar
Private _displayText As String = String.Empty
Public Property DisplayText As String
Get
Return _displayText
End Get
Set(value As String)
If _displayText <> value Then
_displayText = value
' 値が変わったら再描画を要求
Me.Invalidate()
End If
End Set
End Property
End Class
ここでのポイントを表にまとめると次の通りです。
| 要素 | 役割 | 実装上のポイント |
|---|---|---|
_displayText | 実テキストを保持するプライベートフィールド | 外部から直接触らせず、プロパティ経由で操作させる |
DisplayText | デザイナー・コードから参照される公開プロパティ | 値が変わったときだけ Invalidate() を呼んで描画コストを抑える |
Invalidate() | コントロールの再描画を OS に依頼する | WM_PAINT が発行され、後述の WndProc/AdditionalPaint が呼ばれる |
デザイナーで扱いやすくする属性の追加(任意)
プロパティをデザイナーからも設定しやすくするために、Category や Description 属性を付けておくと便利です。
Imports System.ComponentModel
Public Class ProgressBarZ
Inherits ProgressBar
Private _displayText As String = String.Empty
<Category("Appearance"),
Description("ProgressBar 上に表示する文字列")>
Public Property DisplayText As String
Get
Return _displayText
End Get
Set(value As String)
If _displayText <> value Then
_displayText = value
Me.Invalidate()
End If
End Set
End Property
End Class
これで、フォームデザイナーのプロパティウィンドウにも DisplayText が表示され、デザイン時にテキストを試しながら調整することもできます。
ProgressBar 上に文字列を描画する AdditionalPaint の実装
次に、バーの上に文字列を描画する部分です。ここでは、標準の ProgressBar に描画させた後、WM_PAINT メッセージをフックしてテキストだけを重ね描きするスタイルを取ります。
WM_PAINT をフックする WndProc のオーバーライド
まずは WndProc をオーバーライドし、WM_PAINT メッセージの後に追加描画メソッドを呼び出します。
Protected Overrides Sub WndProc(ByRef m As Message)
MyBase.WndProc(m)
Const WM_PAINT As Integer = &H0F
If m.Msg = WM_PAINT Then
AdditionalPaint()
End If
End Sub
MyBase.WndProc(m) を先に呼んでいる点が重要です。これにより、まず標準の ProgressBar が描画され、その上からテキストを描くことができます。
AdditionalPaint で DisplayText を描画する
続いて、実際に文字列を描画する AdditionalPaint メソッドを実装します。
Private Sub AdditionalPaint()
' テキストが空なら何もしない
If String.IsNullOrEmpty(_displayText) Then
Return
End If
Using g As Graphics = Graphics.FromHwnd(Me.Handle)
Dim rect As New Rectangle(0, 0, Me.Width, Me.Height)
Using fmt As New StringFormat(StringFormatFlags.NoWrap)
fmt.Alignment = StringAlignment.Center
fmt.LineAlignment = StringAlignment.Center
Using br As New SolidBrush(Me.ForeColor)
g.DrawString(_displayText, Me.Font, br, rect, fmt)
End Using
End Using
End Using
End Sub
このメソッドでは、
Graphics.FromHwnd(Handle)でコントロールのデバイスコンテキストを取得- コントロール全体を表す矩形
Rectangle(0, 0, Width, Height)を用意 StringFormatを使って水平方向・垂直方向とも中央寄せに設定ForeColorとFontを利用してテキストを描画
という処理を行っています。
| プロパティ / メソッド | 役割 | ポイント |
|---|---|---|
Graphics.FromHwnd | コントロールの描画対象を表す Graphics を取得 | 取得した Graphics は必ず Using で破棄する |
StringFormat.Alignment | 水平方向の揃え位置 | StringAlignment.Center で中央寄せ |
StringFormat.LineAlignment | 垂直方向の揃え位置 | StringAlignment.Center で上下中央寄せ |
SolidBrush(ForeColor) | 文字色を指定 | フォーム側から ForeColor を変えれば文字色も変わる |
ProgressBarZ クラスの全体コード
ここまでの内容をまとめると、基本形となる ProgressBarZ クラスは次のようになります。
Imports System.ComponentModel
Imports System.Drawing
Imports System.Windows.Forms
Public Class ProgressBarZ
Inherits ProgressBar
Private _displayText As String = String.Empty
<Category("Appearance"),
Description("ProgressBar 上に表示する文字列")>
Public Property DisplayText As String
Get
Return _displayText
End Get
Set(value As String)
If _displayText <> value Then
_displayText = value
Me.Invalidate() ' 文字列が変わったら再描画
End If
End Set
End Property
Protected Overrides Sub WndProc(ByRef m As Message)
MyBase.WndProc(m)
Const WM_PAINT As Integer = &H0F
If m.Msg = WM_PAINT Then
AdditionalPaint()
End If
End Sub
Private Sub AdditionalPaint()
If String.IsNullOrEmpty(_displayText) Then
Return
End If
Using g As Graphics = Graphics.FromHwnd(Me.Handle)
Dim rect As New Rectangle(0, 0, Me.Width, Me.Height)
Using fmt As New StringFormat(StringFormatFlags.NoWrap)
fmt.Alignment = StringAlignment.Center
fmt.LineAlignment = StringAlignment.Center
Using br As New SolidBrush(Me.ForeColor)
g.DrawString(_displayText, Me.Font, br, rect, fmt)
End Using
End Using
End Using
End Sub
End Class
この状態でフォームに配置し、
ProgressBarZ1.Value = 30
ProgressBarZ1.DisplayText = "30 % 完了"
と設定すれば、バーの中央に「30 % 完了」と表示されるようになります。
DisplayText を動的に更新する例
次に、進捗とテキストをリアルタイムで更新する簡単なサンプルを示します。
フォーム側のサンプルコード
フォームに ProgressBarZ と Timer を配置し、一定間隔で進捗とテキストを更新してみます。
Public Class Form1
Private Sub Form1_Load(sender As Object, e As EventArgs) _
Handles MyBase.Load
ProgressBarZ1.Minimum = 0
ProgressBarZ1.Maximum = 100
ProgressBarZ1.Value = 0
ProgressBarZ1.DisplayText = "処理待機中"
Timer1.Interval = 100 ' 100ms ごとに更新
Timer1.Start()
End Sub
Private Sub Timer1_Tick(sender As Object, e As EventArgs) _
Handles Timer1.Tick
If ProgressBarZ1.Value < ProgressBarZ1.Maximum Then
ProgressBarZ1.Value += 1
ProgressBarZ1.DisplayText =
ProgressBarZ1.Value.ToString() & " % 完了"
Else
ProgressBarZ1.DisplayText = "完了しました"
Timer1.Stop()
End If
End Sub
End Class
この例では、進捗値とテキストを同じタイミングで更新しているため、「バーの長さ」と「数字の表示」が常に同期した状態で動きます。
バックグラウンドスレッドから更新する場合の注意点
重い処理を Task や BackgroundWorker で実行しつつ、途中経過を DisplayText に表示したくなるケースも多いです。このとき注意しなければならないのがスレッド境界です。
- UI スレッド以外から
DisplayTextを直接書き換えるのは NG InvokeまたはBeginInvokeを使って UI スレッドに処理を渡す
例えば、次のような書き方になります。
Private Sub ReportProgress(value As Integer, message As String)
If ProgressBarZ1.InvokeRequired Then
ProgressBarZ1.Invoke(
Sub()
ProgressBarZ1.Value = value
ProgressBarZ1.DisplayText = message
End Sub)
Else
ProgressBarZ1.Value = value
ProgressBarZ1.DisplayText = message
End If
End Sub
これにより、バックグラウンド処理中でも安全に ProgressBarZ の表示を更新できます。
テキスト表示をカスタマイズするアイデア
基本の実装ができたら、用途に合わせてテキスト表示をカスタマイズするのもおすすめです。
テキストの見た目を変える(フォント・色・配置)
すでに実装しているように、フォントと色はコントロールの Font / ForeColor を利用しているため、フォーム側から簡単に変更できます。
ProgressBarZ1.Font = New Font("Meiryo", 10, FontStyle.Bold)
ProgressBarZ1.ForeColor = Color.White
配置についても、StringFormat.Alignment と LineAlignment を変えることで、左寄せ・右寄せなどが可能です。
fmt.Alignment = StringAlignment.Near ' 左寄せ
fmt.LineAlignment = StringAlignment.Far ' 下寄せ
用途に応じて、例えば「進捗率は右下、小さめの文字で表示」など、UI デザインに合わせた調整ができます。
テキストを一時的に非表示にする
テキストが不要な区間では DisplayText を空文字列にするだけで、描画をスキップできます。
ProgressBarZ1.DisplayText = String.Empty
' または
ProgressBarZ1.DisplayText = Nothing
AdditionalPaint の先頭で空チェックをしているため、この状態では文字列は描画されません。
チラつきを抑える描画設定(任意)
ProgressBar に限らず、カスタム描画を行うと再描画のたびにチラつきが発生することがあります。これを軽減するために、次のような設定を追加することができます。
SetStyle による描画スタイルの設定
ControlStyles のフラグを指定して、ダブルバッファリングなどを有効にします。ただし、ProgressBar を完全に自前描画する場合と組み合わせて使うことが多いので、ここでは「導入例」として紹介します。
Public Sub New()
MyBase.New()
Me.SetStyle(ControlStyles.UserPaint, True)
Me.SetStyle(ControlStyles.AllPaintingInWmPaint, True)
Me.SetStyle(ControlStyles.OptimizedDoubleBuffer, True)
End Sub
UserPaint = True にすると、標準の ProgressBar の描画ではなく、OnPaint を使って完全に自前で描画する形になります。バーそのものの見た目を自由に描きたい場合に有効ですが、既存の標準見た目を活かしたい場合は、この記事前半で解説した WndProc フック方式でも十分です。
CreateParams の ExStyle に WS_EX_TRANSPARENT を与える
透過的に描画することで、再描画時のちらつきを抑えられることがあります。CreateParams をオーバーライドして、拡張スタイルを変更します。
Protected Overrides ReadOnly Property CreateParams As CreateParams
Get
Dim cp As CreateParams = MyBase.CreateParams
Const WS_EX_TRANSPARENT As Integer = &H20
cp.ExStyle = cp.ExStyle Or WS_EX_TRANSPARENT
Return cp
End Get
End Property
環境によっては、意図しない再描画順序になる場合もあるため、「ちらつきがひどい場合の候補」として試しながら調整するとよいでしょう。
Label を重ねる方法との比較
ProgressBar に文字を表示する一般的な別案として、ProgressBar の上に Label を重ねて配置するという方法もあります。それぞれの方法のメリットを簡単に比較しておきます。
| 方式 | メリット | デメリット |
|---|---|---|
| Label を重ねる | 実装が簡単で、GDI+ コードを書かなくてよい テキストのフォントや色を Label 側で細かく設定しやすい | レイアウトがずれやすく、リサイズ時の調整が面倒 見た目の一体感がやや劣る場合がある |
| ProgressBarZ で描画 | 単一コントロールとして扱えるため、再利用性が高い 描画タイミングと同期しているため、チラつきやずれが少ない 進捗バーのスタイルを拡張したり、アニメーションを追加しやすい | GDI+ のコードに慣れていないと少しハードルが高い 環境によってはチラつき対策など調整が必要 |
「とりあえず動けばいい」という段階では Label 重ね方式も有効ですが、複数画面で使い回したい、デザインを統一したい、メンテナンス性を高めたいといった要件がある場合は、この記事で紹介した ProgressBarZ のようなカスタムコントロール化が圧倒的に有利です。
よくあるつまずきポイントと対処法
テキストが更新されない/昔の文字が残る
主な原因は次のどちらかです。
DisplayTextのSetでInvalidate()を呼んでいない- 変更後の文字列が以前と同じなので、
Invalidate()が実行されていない
前者はコードの書き漏れなので修正が必要です。後者の場合、
- 進捗に応じて必ず異なる文字列になるようにする(例:
"50 % 完了") - 同じ文字列でも再描画したい場面では、
ProgressBarZ1.Invalidate()を明示的に呼ぶ
といった工夫で対応できます。
背景色とのコントラストが弱く、文字が見えづらい
標準の ProgressBar は緑系のバーがデフォルトのため、黒い文字でも視認性が高いケースが多いですが、カスタムテーマや OS の設定によっては見えづらくなります。次のような対策が考えられます。
- 文字色を白 (
Color.White) にしてコントラストを上げる - 太字フォントにして視認性を確保する
- ドロップシャドウ風に、黒と白の 2 回描画を行う
例えば、簡易的な縁取りをする場合は、次のように 2 回描画すると読みやすくなります。
Using g As Graphics = Graphics.FromHwnd(Me.Handle)
Dim rect As New Rectangle(0, 0, Me.Width, Me.Height)
Using fmt As New StringFormat(StringFormatFlags.NoWrap)
fmt.Alignment = StringAlignment.Center
fmt.LineAlignment = StringAlignment.Center
' 影(黒っぽい色)を少しずらして描画
Using shadow As New SolidBrush(Color.FromArgb(128, Color.Black))
Dim shadowRect = Rectangle.Inflate(rect, -1, -1)
g.DrawString(_displayText, Me.Font, shadow, shadowRect, fmt)
End Using
' 本来の文字(白)
Using br As New SolidBrush(Me.ForeColor)
g.DrawString(_displayText, Me.Font, br, rect, fmt)
End Using
End Using
End Using
このように描画を少し工夫するだけで、視認性の高い ProgressBar に仕上げることができます。
まとめ:ProgressBarZ で「伝わる」進捗表示を実現する
本記事では、Windows Forms(VB.NET)の ProgressBar を拡張したカスタムクラス ProgressBarZ を例に、進捗バー上に任意のテキストを動的に重ねて表示する方法を解説しました。
DisplayTextプロパティを追加し、文字列を外部から自由に設定できるようにする- プロパティの
SetでInvalidate()を呼び、変更を確実に再描画へ反映させる WndProcでWM_PAINTをフックし、AdditionalPaintでバー上にテキストを描画する- フォント・文字色・配置は
Font/ForeColorやStringFormatで柔軟に変更可能 - 必要に応じて描画スタイルや
CreateParamsを調整し、チラつきを抑えることもできる
単純なバー表示だけではユーザーに伝わりにくかった進捗状況も、"50 % 完了" や "ファイルのコピー中..." といった明確なメッセージを重ねることで、分かりやすく・安心感のある UI に変えることができます。
既存プロジェクトの ProgressBar を置き換えるだけでも効果は大きいので、ぜひ ProgressBarZ のようなカスタムコントロールを自作して、プロジェクト共通の「使いやすい進捗表示コンポーネント」として育ててみてください。

コメント