VB.NET/WinFormsでProgressBarを「縦向き」にしつつ、Vista以降の3状態(緑/黄/赤)で使いたい──監視ツールやダッシュボードを作っていると、そんなニーズがよく出てきます。しかし素直に派生クラスを作るだけでは、縦向きが効かなかったり、色の再描画が崩れたりして「なぜ?」となりがちです。本記事では、その理由と実務で使える2つの実装パターン(既存ProgressBar派生+自前描画カスタム)を詳しく解説します。
WinForms ProgressBarで「縦向き+3状態」を両立させたい
対象環境は以下を想定しています。
- .NET Framework 4.8
- VB.NET
- Windows Forms(WinForms)
- OSは Windows Vista 以降
標準の ProgressBar は、フォームデザイナから簡単に使える一方で、以下のような制限があります。
- 縦向き(Vertical)のプロパティは用意されていない
- 3状態(緑=Normal/黄=Paused/赤=Error)の切り替えは、Win32メッセージ(
PBM_SETSTATE)を自力で送る必要がある
そのため多くの場合、ProgressBar派生クラスを作って、
- 縦向きスタイル(
PBS_VERTICAL)の付与 PBM_SETSTATEによる色状態の切り替え
を実装することになります。しかしこれをそのまま実装すると、次のような問題に遭遇しがちです。
- 縦向きに切り替えても反映されない(横のまま)
- 状態を切り替えても色が変わらない/再描画が崩れる
まずは、この「うまくいかない理由」を整理しておきましょう。
なぜ「縦向き」や「3色」がうまく動かないのか
Win32のProgressBarコントロール(PROGRESS_CLASS)は、スタイルや色に関していくつかの決まりごとがあります。特にハマりやすいのが次の2点です。
PBS_VERTICALは「作成時スタイル」であり、ハンドル再作成が必須- 3状態の色はVisual Styles依存&再描画が不安定になりやすい
| 症状 | 主な原因 | 対策の方向性 |
|---|---|---|
| 縦向きにならない | PBS_VERTICAL をハンドル作成後に変えようとしている | CreateParams でスタイルを付与し、変更時は RecreateHandle() を呼ぶ |
| 赤や黄に切り替わらない | PBM_SETSTATE を送っていない/ハンドル未作成時に送っている | IsHandleCreated を確認し、OnHandleCreated で再適用する |
| 色の再描画が崩れる | テーマや環境依存の既知の不安定さ | 再描画誘発の工夫、または自前描画コントロールに切り替える |
以下では、実装パターンを2つに分けて解説します。
- アプローチ1:既存
ProgressBar派生クラスを最小修正して使う - アプローチ2:描画型カスタムコントロール(
Control継承)で完全に制御する
短期的にはアプローチ1、長期運用や高品質UIを目指すならアプローチ2、という使い分けがおすすめです。
アプローチ1:既存ProgressBar派生を最小修正して縦向き+3色対応
まずは、標準の ProgressBar を継承した派生クラスに、縦向きと3状態を追加する方法です。既存コードやデザイナとの相性が良く、導入の敷居が低いのが利点です。
- メリット:実装が軽い/OS標準の3色(緑・黄・赤)をそのまま使える
- デメリット:環境によっては色の再描画が不安定なことがある
PBS_VERTICALは「作成時スタイル」なのでRecreateHandleが必須
縦向きのスタイル PBS_VERTICAL(値は &H4)は、ハンドル生成時に決定される「作成時スタイル」です。そのため、プロパティで縦向きを切り替えたときは、RecreateHandle() を呼んでハンドルを作り直す必要があります。
Imports System.Windows.Forms
Public Class VerticalProgressBarEx
Inherits ProgressBar
Private _vertical As Boolean
''' <summary>
''' 縦向きにするかどうか
''' </summary>
Public Property ProgressVertical As Boolean
Get
Return _vertical
End Get
Set(value As Boolean)
If _vertical <> value Then
_vertical = value
' ★作成時スタイル変更のためハンドル再作成が必須
If IsHandleCreated Then
RecreateHandle()
End If
End If
End Set
End Property
Protected Overrides ReadOnly Property CreateParams As CreateParams
Get
Dim cp = MyBase.CreateParams
If _vertical Then
Const PBS_VERTICAL As Integer = &H4
cp.Style = cp.Style Or PBS_VERTICAL
End If
Return cp
End Get
End Property
End Class
よくある勘違いとして、「Style プロパティを動的に変えれば縦になるのでは?」というパターンがありますが、Win32コントロールの多くはスタイルをハンドル作成後には反映できない仕様です。CreateParams でスタイルを定義し、変更時に RecreateHandle() を呼ぶ、というのが正しい筋です。
3状態(緑/黄/赤)の切り替えをPBM_SETSTATEで行う
次に、PBM_SETSTATE メッセージを使って色の状態を切り替えます。Win32的には以下の3状態が定義されています。
PBST_NORMAL(1):通常(緑)PBST_ERROR(2):エラー(赤)PBST_PAUSED(3):一時停止(黄)
これを VB.NET から扱うための派生クラス例がこちらです。
Imports System.Runtime.InteropServices
Imports System.Windows.Forms
Public Class VerticalProgressBarEx
Inherits ProgressBar
Private _vertical As Boolean
Private _state As MeterState = MeterState.Normal
Public Enum MeterState
Normal = 1 ' PBST_NORMAL(緑)
[Error] = 2 ' PBST_ERROR(赤)
Paused = 3 ' PBST_PAUSED(黄)
End Enum
<DllImport("user32.dll", CharSet:=CharSet.Auto)>
Private Shared Function SendMessage(
hWnd As IntPtr,
msg As Integer,
wParam As IntPtr,
lParam As IntPtr
) As IntPtr
End Function
Private Const WM_USER As Integer = &H400
Private Const PBM_SETSTATE As Integer = WM_USER + 16
Public Property ProgressVertical As Boolean
Get
Return _vertical
End Get
Set(value As Boolean)
If _vertical <> value Then
_vertical = value
If IsHandleCreated Then
RecreateHandle()
End If
End If
End Set
End Property
Public Property State As MeterState
Get
Return _state
End Get
Set(value As MeterState)
If _state <> value Then
_state = value
ApplyState()
End If
End Set
End Property
Protected Overrides ReadOnly Property CreateParams As CreateParams
Get
Dim cp = MyBase.CreateParams
If _vertical Then
Const PBS_VERTICAL As Integer = &H4
cp.Style = cp.Style Or PBS_VERTICAL
End If
Return cp
End Get
End Property
Protected Overrides Sub OnHandleCreated(e As EventArgs)
MyBase.OnHandleCreated(e)
' ハンドル再作成時に状態を再適用しておく
ApplyState()
End Sub
Private Sub ApplyState()
If IsHandleCreated Then
SendMessage(Me.Handle, PBM_SETSTATE, CType(_state, IntPtr), IntPtr.Zero)
Invalidate()
End If
End Sub
End Class
このクラスをフォームに貼り付けて使えば、
ProgressVertical = True ' 縦向きにする
State = MeterState.Paused ' 黄(保留状態)
Value = 50 ' 50%
といった指定で、縦向き&黄色ProgressBarが表示されるようになります。
Visual Styles前提であることに注意
3状態の色は、Visual Styles(テーマ)前提です。通常はアプリケーション起動時に
Application.EnableVisualStyles()
を呼び出していれば問題ありませんが、サーバーOSやテーマ無効環境では、色が意図通りにならないことがあります。この場合は、アプリ側の仕様として「3色表示はテーマ有効時のみ」と割り切るか、後述する自前描画コントロールに切り替えるのが現実的です。
色の再描画が不安定なときのワークアラウンド
環境やテーマによって、状態を切り替えても色が反映されない/残像が残ることがあります。これはWin32 ProgressBar側の既知の挙動で、アプリ側でできる対策は「再描画を強制的に誘発する」程度です。
- 一度
State = MeterState.Normalに戻してから、目的の状態に再設定する Valueを一時的に +1 / -1 して戻し、描画更新のトリガーにするInvalidate()に加えてUpdate()やRefresh()を呼ぶ
ただし、これらはいずれも根本対策ではなく「おまじない」に近いので、長期的には次章の「描画型カスタムコントロール」への移行を検討した方が安全です。
フォーム側の利用例
最後に、フォームからの簡単な利用例です。
Public Class Form1
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
' デザイナで配置している場合は不要
Dim meter As New VerticalProgressBarEx() With {
.ProgressVertical = True,
.State = VerticalProgressBarEx.MeterState.Normal,
.Minimum = 0,
.Maximum = 100,
.Value = 30,
.Size = New Size(30, 150),
.Location = New Point(20, 20)
}
Me.Controls.Add(meter)
End Sub
Private Sub btnError_Click(sender As Object, e As EventArgs) Handles btnError.Click
ProgressBarEx1.State = VerticalProgressBarEx.MeterState.Error
ProgressBarEx1.Value = 100
End Sub
Private Sub btnPause_Click(sender As Object, e As EventArgs) Handles btnPause.Click
ProgressBarEx1.State = VerticalProgressBarEx.MeterState.Paused
ProgressBarEx1.Value = 50
End Sub
End Class
簡単な状態表示や、既存フォームへの組み込み程度であれば、このアプローチで十分なケースが多いでしょう。
アプローチ2:描画型カスタムコントロールで安定動作を優先(推奨)
もう一つのパターンは、Control(または UserControl)を継承して、描画をすべて自前で行う方法です。OS標準の3色はそのまま使えませんが、
- 縦向き・横向き
- カラー(緑・黄・赤・その他任意色)
- バーの太さ・角丸・グラデーション
- テキスト表示や閾値による色切り替え
などを完全に制御できます。色の再描画問題からも解放されるため、業務アプリや監視UIではこちらを選ぶ価値が非常に高いです。
Control継承+UserPaintで自前描画する基本形
描画型コントロールの基本パターンは次のようになります。
Imports System.Windows.Forms
Imports System.Drawing
Imports System.Windows.Forms.VisualStyles
Public Class MeterBarEx
Inherits Control
Public Enum MeterState
Normal
[Error]
Paused
End Enum
Private _maximum As Integer = 100
Private _minimum As Integer = 0
Private _value As Integer = 0
Private _vertical As Boolean = False
Private _state As MeterState = MeterState.Normal
Public Sub New()
' 自前描画の有効化+ダブルバッファリング
SetStyle(ControlStyles.UserPaint Or
ControlStyles.AllPaintingInWmPaint Or
ControlStyles.OptimizedDoubleBuffer Or
ControlStyles.ResizeRedraw,
True)
DoubleBuffered = True
End Sub
Public Property Minimum As Integer
Get
Return _minimum
End Get
Set(value As Integer)
If value <= _maximum Then
_minimum = value
If _value < _minimum Then _value = _minimum
Invalidate()
End If
End Set
End Property
Public Property Maximum As Integer
Get
Return _maximum
End Get
Set(value As Integer)
If value >= _minimum Then
_maximum = value
If _value > _maximum Then _value = _maximum
Invalidate()
End If
End Set
End Property
Public Property MeterValue As Integer
Get
Return _value
End Get
Set(value As Integer)
Dim v = Math.Max(_minimum, Math.Min(_maximum, value))
If _value <> v Then
_value = v
Invalidate()
End If
End Set
End Property
Public Property Vertical As Boolean
Get
Return _vertical
End Get
Set(value As Boolean)
If _vertical <> value Then
_vertical = value
Invalidate()
End If
End Set
End Property
Public Property State As MeterState
Get
Return _state
End Get
Set(value As MeterState)
If _state <> value Then
_state = value
Invalidate()
End If
End Set
End Property
Protected Overrides Sub OnPaint(e As PaintEventArgs)
MyBase.OnPaint(e)
Dim g = e.Graphics
g.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias
Dim rect = Rectangle.Inflate(Me.ClientRectangle, -2, -2)
' 溝(背景)を描画
DrawTrough(g, rect)
' 進捗率を0.0~1.0に正規化
Dim range = Math.Max(1, _maximum - _minimum)
Dim pct As Double = (MeterValue - _minimum) / CDbl(range)
pct = Math.Max(0.0, Math.Min(1.0, pct))
If pct <= 0 Then
Return
End If
' 進捗バー矩形の計算(縦/横)
Dim fill As Rectangle
If _vertical Then
Dim h = CInt(rect.Height * pct)
fill = New Rectangle(rect.Left, rect.Bottom - h, rect.Width, h)
Else
Dim w = CInt(rect.Width * pct)
fill = New Rectangle(rect.Left, rect.Top, w, rect.Height)
End If
' 状態に応じた色
Dim barColor As Color
Select Case _state
Case MeterState.Paused
barColor = Color.Goldenrod ' 黄
Case MeterState.Error
barColor = Color.Firebrick ' 赤
Case Else
barColor = Color.ForestGreen ' 緑
End Select
Using b As New SolidBrush(barColor)
g.FillRectangle(b, fill)
End Using
End Sub
Private Sub DrawTrough(g As Graphics, rect As Rectangle)
' OSテーマが使えるならProgressBarRendererを利用
If ProgressBarRenderer.IsSupported Then
If _vertical Then
ProgressBarRenderer.DrawVerticalBar(g, Me.ClientRectangle)
Else
ProgressBarRenderer.DrawHorizontalBar(g, Me.ClientRectangle)
End If
Else
' テーマが使えない環境用の簡易描画
g.Clear(Me.BackColor)
ControlPaint.DrawBorder(g, rect, SystemColors.ControlDark, ButtonBorderStyle.Solid)
End If
End Sub
End Class
ポイントは次の通りです。
ControlStyles.UserPaintを有効にして、完全自前描画にするResizeRedrawを有効にして、サイズ変更時も自動再描画- テーマが有効なときは
ProgressBarRendererを使って「枠だけ」OS風に描画 - 中身(充填部分)の色は、
Stateと進捗率から自前で塗りつぶす
縦向き・横向きの切り替えロジック
上の例では、縦向きフラグ Vertical に応じて、バーの塗り方向を変えています。
- 横向き:左から右へ充填
- 縦向き:下から上へ充填
計算式部分だけ抜き出すと、以下のようになります。
Dim fill As Rectangle
If _vertical Then
Dim h = CInt(rect.Height * pct)
fill = New Rectangle(rect.Left, rect.Bottom - h, rect.Width, h)
Else
Dim w = CInt(rect.Width * pct)
fill = New Rectangle(rect.Left, rect.Top, w, rect.Height)
End If
このように、「描画用の矩形をどう計算するか」だけで縦向き対応は完結します。Win32の PBS_VERTICAL を意識する必要はありません。
業務アプリ向けの拡張アイデア
描画型コントロールにしておくと、以下のような拡張が非常に簡単になります。
| 拡張内容 | 概要 | 実装のヒント |
|---|---|---|
| 数値テキスト表示 | バーの中央に「75%」「3/10」などを描画 | Graphics.DrawString を使用し、StringFormat で中央寄せ |
| 閾値で自動色変更 | 80%以上で赤、50~79%で黄、他は緑など | State を外から設定する代わりに、MeterValue に応じて内部で決定 |
| アニメーション | 一定周期でValueを増減させる「バウンド」表示 | TimerコントロールでValueを書き換え、Invalidate()を呼ぶ |
| 角丸・グラデーション | 見た目をモダンにしたい場合 | GraphicsPath と LinearGradientBrush を使う |
これらは、Win32 ProgressBarのスタイルやテーマに縛られていると実現しづらい部分です。描画型コントロールにしてしまうことで、実装コストと引き換えに、UIの自由度と保守性を大きく向上させられます。
フォームからの利用例
Public Class Form2
Private _meter As MeterBarEx
Private Sub Form2_Load(sender As Object, e As EventArgs) Handles MyBase.Load
_meter = New MeterBarEx() With {
.Vertical = True,
.State = MeterBarEx.MeterState.Normal,
.Minimum = 0,
.Maximum = 100,
.MeterValue = 20,
.Size = New Size(40, 180),
.Location = New Point(20, 20)
}
Controls.Add(_meter)
End Sub
Private Sub btnUp_Click(sender As Object, e As EventArgs) Handles btnUp.Click
_meter.MeterValue = Math.Min(_meter.Maximum, _meter.MeterValue + 10)
End Sub
Private Sub btnDown_Click(sender As Object, e As EventArgs) Handles btnDown.Click
_meter.MeterValue = Math.Max(_meter.Minimum, _meter.MeterValue - 10)
End Sub
Private Sub btnPause_Click(sender As Object, e As EventArgs) Handles btnPause.Click
_meter.State = MeterBarEx.MeterState.Paused
End Sub
Private Sub btnError_Click(sender As Object, e As EventArgs) Handles btnError.Click
_meter.State = MeterBarEx.MeterState.Error
End Sub
Private Sub btnNormal_Click(sender As Object, e As EventArgs) Handles btnNormal.Click
_meter.State = MeterBarEx.MeterState.Normal
End Sub
End Class
複数のメーターを並べてサーバー負荷を監視したり、各種センサー値のビジュアル表示を行ったりといった用途に、そのまま流用できます。
実装時の「落とし穴」と回避策
ProgressBarをカスタムする際にハマりやすいポイントを、アプローチ1/2共通でまとめておきます。
| ポイント | 内容 | 推奨対策 |
|---|---|---|
Style = Continuousの挙動 | テーマの有無やOSバージョンによって見た目が変わる | 見た目を厳密に統一したい場合は、描画型コントロールに切り替える |
| UserPaintの無視 | ProgressBar継承では ControlStyles.UserPaint が無視される | 完全自前描画が必要なら、Control/UserControl を継承する |
| 3状態色のVisual Styles依存 | テーマ無効時やリモート環境では、緑/黄/赤にならない場合がある | テーマ前提で使うか、描画型で Color を自前指定する |
| DPIスケーリング | 高DPI環境でフォーム全体のスケールが変わる | 描画型の場合は ClientRectangle から相対計算することで、基本的に自動追従可能 |
| 再描画のタイミング | タイマー更新やバックグラウンド処理から頻繁に更新する場合 | Invoke/BeginInvokeでUIスレッドから更新し、必要に応じて Invalidate() のみ行う |
どちらのアプローチを選ぶべきか
最後に、「既存ProgressBar派生」と「描画型カスタムコントロール」の使い分けを整理します。
既存ProgressBar派生(アプローチ1)が向いているケース
- 既存フォームで
ProgressBarを大量に使っており、差し替えコストを最小化したい - 縦向きと3色対応ができれば、細かな見た目にはこだわらない
- 主な利用環境がWindows 10/11クライアントで、テーマ無効状態をあまり想定しなくてよい
描画型カスタムコントロール(アプローチ2)が向いているケース
- サーバー監視ツール・工場監視パネルなど、常時表示される重要なUIで使いたい
- 高DPI環境やリモート接続など、多様な環境でも安定表示させたい
- 色やアニメーション、テキスト表示など、見た目を細かくカスタマイズしたい
- 今後も追加機能(閾値表示、点滅、クリック操作など)を拡張していきたい
多くの業務システムでは、最初は「とりあえずアプローチ1で課題を解決し、実際の利用状況を見ながらアプローチ2に移行する」というステップを踏むのが現実的です。特に縦向きProgressBarは、標準コントロールだけでは表現しづらい情報(レベルメーターやタンク残量など)に非常にマッチします。実装パターンを押さえておけば、プロジェクトごとに最適な形で再利用できます。
まとめ:WinFormsで「縦向き+3色」ProgressBarを実現するポイント
- PBS_VERTICALは作成時スタイルなので、プロパティ変更時には必ず
RecreateHandle()でハンドルを作り直す - 3状態(緑/黄/赤)は
PBM_SETSTATEで切り替える。ハンドル再作成時はOnHandleCreatedで再適用する - OSテーマや環境によっては、3状態の色が再描画不安定になることがある
- 安定性と拡張性を重視するなら、
Control継承の描画型カスタムコントロールを選ぶのがベスト - 業務アプリでは、縦向きProgressBarをメーター表示や状態表示に活用することで、一 glanceで状況を把握できるUIを実現しやすくなる
本記事のサンプルコードをベースに、自身のプロジェクトのコーディング規約やデザインに合わせてカスタマイズすれば、「WinFormsでもここまでできるのか」と感じられる、使い勝手のよい縦向き3状態ProgressBarを実装できるはずです。

コメント