VB.NET WinFormsでProgressBarを縦向き+3状態(緑・黄・赤)にする実装徹底解説

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を実装できるはずです。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次