HScrollBar・VScrollBarを使った独自スクロール領域の実装:大量の図面やカスタムマップを描画するUI設計
Windows Forms(以下WinForms)における画面開発において、避けて通れないのが「巨大な描画領域のスクロール制御」である。
特に、CAD図面のベクターデータ、高解像度の航空写真、あるいは製造ラインのリアルタイムトポロジーマップなどを扱う場合、.NET Frameworkが標準で提供する `AutoScroll` 機能に依存することは、パフォーマンスの自殺行為に等しい。標準の `Panel` コントロールなどに巨大なピクチャボックスを配置する設計は、容易に数千万ピクセル規模のメモリ(GDI+バッファ)を消費し、GDIオブジェクトの枯渇やOut Of Memory(OOM)、そして描画遅延による激しいチャタリングを引き起こす。
本稿では、これらのレガシーかつ現代的な課題を解決するため、`HScrollBar`(水平スクロールバー)と`VScrollBar`(垂直スクロールバー)を自前で制御し、仮想座標系と物理座標系を厳密に分離・アフィン変換する「独自スクロールビューアー」の設計・実装極限テクニックを解説する。
—
1. アーキテクチャ設計:仮想座標系と物理(デバイス)座標系の分離
独自のスクロール・ズーム処理を実装する上で、最初に行うべきは「仮想(ワールド)座標系」と「物理(スクリーン)座標系」の完全な分離である。
- 仮想座標系 (World Coordinates):
図面やマップが本来持つ、原点 $(0, 0)$ からの無限の広がりを持つ座標系。単位はピクセル、ミリメートル、あるいは地理的緯度経度。
- 物理座標系 (Device Coordinates):
実際にユーザーのモニター(コントロールの描画領域)に表示される、左上 $(0, 0)$ から右下 $(\text{Width}, \text{Height})$ までの座標系。
座標変換の数理モデル
ある仮想座標 $(X_w, Y_w)$ を物理座標 $(X_d, Y_d)$ に変換するアフィン変換式は、ズーム倍率 $S$(Scale)およびスクロールオフセット $O_x, O_y$(仮想座標空間における表示開始位置)を用いて以下のように定義される。
$$X_d = (X_w – O_x) \times S$$
$$Y_d = (Y_w – O_y) \times S$$
逆に、画面上のマウスクリック位置(物理座標)から図面上の仮想座標を逆算する(ヒットテストやオブジェクト選択を行う)場合は、以下の逆変換式を用いる。
$$X_w = \frac{X_d}{S} + O_x$$
$$Y_w = \frac{Y_d}{S} + O_y$$
この変換を、描画時およびイベントハンドラ内で高速に実行する。
—
2. Windows Formsスクロールバーの「最大値仕様」をハックする
WinFormsの `ScrollBar`(`HScrollBar` / `VScrollBar`)には、Win32 APIの `ScrollInfo` 構造体の設計を引きずった、歴史的な「仕様(バグと混同されやすい挙動)」が存在する。
それは、スクロールバーのつまみ(Thumb)を右端(下端)までドラッグした際の最大値は、`Maximum` ではなく、`Maximum – LargeChange + 1` になるという点である。
[————————————————–] Maximum
|<------------------ Scrollable Range ------------>|
[ LargeChange ]
^
この位置が実際の最大値 (Value)
これを考慮せずに仮想領域のサイズをそのまま `Maximum` に割り当てると、右端や下端に到達できない、あるいは末尾に不自然な余白が残る。
正確なスクロールパラメータの計算式
描画可能な仮想領域の幅を $W_{world}$、現在のコントロールの物理幅を $W_{client}$、ズーム倍率を $S$ としたとき、スクロールバーのパラメータは以下のように動的再計算されなければならない。
1. 仮想全体の物理ピクセルサイズ:
$$W_{total\_device} = W_{world} \times S$$
2. スクロール可能幅(物理):
$$W_{scrollable} = W_{total\_device} – W_{client}$$
3. スクロールバーパラメータの設定:
- $W_{scrollable} \le 0$ の場合、スクロールバーを無効(非表示)にする。
- $W_{scrollable} > 0$ の場合:
- `SmallChange`(矢印クリック時の移動量)= $10$ 〜 $50$ ピクセル(任意の論理単位)
- `LargeChange`(トラッククリック時の移動量)= $W_{client}$(画面幅そのもの)
- `Maximum` = $W_{total\_device}$
- これにより、実際のつまみの最大位置は $W_{total\_device} – W_{client} + 1 \approx W_{scrollable}$ となり、物理的な領域端と完全に一致する。
—
3. メモリ最適化と描画パフォーマンスの極限
大量のオブジェクトを描画する際、.NETの通常の `OnPaint` 内での `e.Graphics.DrawLine` などの単純ループは、数万点を超えた時点でFPS(Frames Per Second)が1桁台に突入する。これを解決するための3つの鉄則を示す。
① クリッピング領域外の「カリング(描画スキップ)」
画面(ビューポート)の物理矩形領域を仮想座標系に逆変換し、その矩形と交差しないオブジェクトは、描画関数の呼び出し(GDI+へのパイプライン投入)自体をスキップする。GDI+は内部的にクリッピングを行うが、関数呼び出しとラッパーオブジェクトの生成コスト自体が巨大なボトルネックになるからである。
② 手動ダブルバッファリングと `BufferedGraphicsContext`
WinFormsの `DoubleBuffered = True` は簡易的で強力だが、高解像度ディスプレイ(4Kなど)においては、システムが内部で確保するバックバッファのサイズが巨大化し、ガベージコレクション(GC)の標的になりやすい。
`BufferedGraphicsManager` を用いて、コントロールの物理サイズに追従したバックバッファを手動で厳密に管理・再利用することで、GCスパイクを極限まで抑制する。
③ GDIオブジェクトの徹底的なライフサイクル管理
`Pen` や `SolidBrush` などを `OnPaint` 内で `New` してはならない。また、システムカラーから取得した `Brushes.Black` などの静的プロパティは解放してはならないが、自身で生成した `Pen` や `Brush`、`Font`、`GraphicsPath` は、必ずスコープを抜ける前に `Dispose` を呼び出すか、`Using` ブロックで囲む。
GDI+オブジェクトは、.NETのマネージドメモリ上は非常に小さなプロキシオブジェクトだが、実体はWin32のアンマネージドなGDI/GDI+リソースを握っている。GCが「まだメモリに余裕がある」と判断してメモリ回収を怠ると、アンマネージド側のGDIハンドル数制限(Windows全体でプロセスあたり最大10,000個)に達し、アプリケーションが沈黙して強制終了する。
—
4. 極限のコード実装(VB.NET / Windows Forms)
以下に、大量の図面要素(デモ用に10万本のランダムライン)をメモリリークなく、超高速にスクロール・ズーム描画する完全なカスタムコントロールの実装を示す。
このコードはそのままプロジェクトに追加し、ツールボックスからフォームに貼り付けて使用可能である。
Imports System.Drawing
Imports System.Drawing.Drawing2D
Imports System.Windows.Forms
”’
”’
Public Class HighPerformanceScrollViewer
Inherits Control
‘ スクロールバーコントロールの明示的配置
Private WithEvents hScrollBar As HScrollBar
Private WithEvents vScrollBar As VScrollBar
‘ 仮想(ワールド)空間のサイズ(例:10,000 x 10,000 ピクセル)
Private ReadOnly virtualWidth As Single = 10000.0F
Private ReadOnly virtualHeight As Single = 10000.0F
‘ ズーム倍率(1.0 = 100%)
Private zoomScale As Single = 1.0F
Private Const MinZoom As Single = 0.1F
Private Const MaxZoom As Single = 10.0F
‘ ダブルバッファリング用グラフィックスコンテキスト
Private bufferContext As BufferedGraphicsContext
Private bufferedGraphics As BufferedGraphics
‘ デモ用:描画オブジェクト(仮想空間上の線分データ)のキャッシュ
Private Structure LineSegment
Public StartPoint As PointF
Public EndPoint As PointF
Public Color As Color
End Structure
Private drawElements As New List(Of LineSegment)()
‘ マウスドラッグ(パンニング)用状態管理
Private isDragging As Boolean = False
Private lastMousePosition As Point
Public Sub New()
‘ コントロールスタイルの最適化
Me.SetStyle(ControlStyles.AllPaintingInWmPaint Or
ControlStyles.UserPaint Or
ControlStyles.Opaque Or ‘ 背景の自動クリアを抑制(チラツキ防止)
ControlStyles.ResizeRedraw, True)
‘ スクロールバーの初期化
Me.hScrollBar = New HScrollBar() With {.Dock = DockStyle.Bottom}
Me.vScrollBar = New VScrollBar() With {.Dock = DockStyle.Right}
Me.Controls.Add(Me.hScrollBar)
Me.Controls.Add(Me.vScrollBar)
‘ ダブルバッファコンテキストの取得
Me.bufferContext = BufferedGraphicsManager.Current
‘ デモデータの生成(10万本のランダムな線分)
GenerateDemoData()
‘ スクロール範囲の初期セットアップ
UpdateScrollBars()
End Sub
Private Sub GenerateDemoData()
Dim rand As New Random(42) ‘ 再現性確保用のシード固定
For i As Integer = 0 To 99999
Dim x1 As Single = CSng(rand.NextDouble()) virtualWidth
Dim y1 As Single = CSng(rand.NextDouble()) virtualHeight
Dim x2 As Single = x1 + CSng(rand.NextDouble() 200.0 – 100.0)
Dim y2 As Single = y1 + CSng(rand.NextDouble() 200.0 – 100.0)
Dim col As Color = Color.FromArgb(128, rand.Next(50, 200), rand.Next(50, 200), rand.Next(50, 200))
drawElements.Add(New LineSegment() With {.StartPoint = New PointF(x1, y1), .EndPoint = New PointF(x2, y2), .Color = col})
Next
End Sub
”’
”’
Private Sub ReallocateBuffer()
If Me.Width <= 0 OrElse Me.Height <= 0 Then Return
If Me.bufferedGraphics IsNot Microsoft.VisualBasic.ControlChars.NullChar Then
Me.bufferedGraphics.Dispose()
End If
' スクロールバーの領域を除いた描画可能領域でバッファを作成
Dim renderWidth As Integer = Math.Max(1, Me.Width - Me.vScrollBar.Width)
Dim renderHeight As Integer = Math.Max(1, Me.Height - Me.hScrollBar.Height)
Me.bufferedGraphics = Me.bufferContext.Allocate(Me.CreateGraphics(), New Rectangle(0, 0, renderWidth, renderHeight))
End Sub
'''
”’
Private Sub UpdateScrollBars()
Dim clientW As Integer = Me.Width – If(vScrollBar.Visible, vScrollBar.Width, 0)
Dim clientH As Integer = Me.Height – If(hScrollBar.Visible, hScrollBar.Height, 0)
‘ ズーム適用後の物理サイズ
Dim totalPhysicalWidth As Integer = CInt(Math.Round(virtualWidth zoomScale))
Dim totalPhysicalHeight As Integer = CInt(Math.Round(virtualHeight zoomScale))
‘ 水平スクロールバーの設定
If totalPhysicalWidth > clientW Then
hScrollBar.Visible = True
hScrollBar.Minimum = 0
hScrollBar.Maximum = totalPhysicalWidth
hScrollBar.LargeChange = clientW
hScrollBar.SmallChange = 16
Else
hScrollBar.Visible = False
hScrollBar.Value = 0
End If
‘ 垂直スクロールバーの設定
If totalPhysicalHeight > clientH Then
vScrollBar.Visible = True
vScrollBar.Minimum = 0
vScrollBar.Maximum = totalPhysicalHeight
vScrollBar.LargeChange = clientH
vScrollBar.SmallChange = 16
Else
vScrollBar.Visible = False
vScrollBar.Value = 0
End If
‘ スクロール位置の境界チェック
NormalizeScrollValues()
‘ バッファの再構築が必要
ReallocateBuffer()
End Sub
Private Sub NormalizeScrollValues()
If hScrollBar.Visible Then
Dim maxVal As Integer = hScrollBar.Maximum – hScrollBar.LargeChange + 1
If hScrollBar.Value > maxVal Then hScrollBar.Value = Math.Max(0, maxVal)
End If
If vScrollBar.Visible Then
Dim maxVal As Integer = vScrollBar.Maximum – vScrollBar.LargeChange + 1
If vScrollBar.Value > maxVal Then vScrollBar.Value = Math.Max(0, maxVal)
End If
End Sub
‘ スクロールバーのイベントハンドラ
Private Sub ScrollBar_ValueChanged(sender As Object, e As EventArgs) Handles hScrollBar.ValueChanged, vScrollBar.ValueChanged
Me.Invalidate()
End Sub
Protected Overrides Sub OnResize(e As EventArgs)
MyBase.OnResize(e)
UpdateScrollBars()
Me.Invalidate()
End Sub
”’
”’
Protected Overrides Sub OnMouseWheel(e As MouseEventArgs)
MyBase.OnMouseWheel(e)
Dim oldZoom As Single = zoomScale
Dim zoomFactor As Single = 1.1F
If e.Delta > 0 Then
zoomScale = Math.Min(MaxZoom, zoomScale zoomFactor)
Else
zoomScale = Math.Max(MinZoom, zoomScale / zoomFactor)
End If
If oldZoom <> zoomScale Then
‘ マウスカーソルの下にある仮想座標を維持するようにスクロール位置を調整する(アンカーズーム)
Dim viewX As Single = e.X
Dim viewY As Single = e.Y
Dim worldX As Single = viewX / oldZoom + (hScrollBar.Value / oldZoom)
Dim worldY As Single = viewY / oldZoom + (vScrollBar.Value / oldZoom)
UpdateScrollBars()
‘ 新しいスクロール位置を逆算してセット
If hScrollBar.Visible Then
Dim newScrollX As Integer = CInt(Math.Round((worldX – (viewX / zoomScale)) zoomScale))
hScrollBar.Value = Math.Max(0, Math.Min(hScrollBar.Maximum – hScrollBar.LargeChange + 1, newScrollX))
End If
If vScrollBar.Visible Then
Dim newScrollY As Integer = CInt(Math.Round((worldY – (viewY / zoomScale)) zoomScale))
vScrollBar.Value = Math.Max(0, Math.Min(vScrollBar.Maximum – vScrollBar.LargeChange + 1, newScrollY))
End If
Me.Invalidate()
End If
End Sub
‘ マウスドラッグ(パン)の実実装
Protected Overrides Sub OnMouseDown(e As MouseEventArgs)
MyBase.OnMouseDown(e)
If e.Button = MouseButtons.Left Then
isDragging = True
lastMousePosition = e.Location
Me.Cursor = Cursors.Hand
End If
End Sub
Protected Overrides Sub OnMouseMove(e As MouseEventArgs)
MyBase.OnMouseMove(e)
If isDragging Then
Dim dx As Integer = e.X – lastMousePosition.X
Dim dy As Integer = e.Y – lastMousePosition.Y
If hScrollBar.Visible AndAlso dx <> 0 Then
Dim newVal As Integer = hScrollBar.Value – dx
hScrollBar.Value = Math.Max(0, Math.Min(hScrollBar.Maximum – hScrollBar.LargeChange + 1, newVal))
End If
If vScrollBar.Visible AndAlso dy <> 0 Then
Dim newVal As Integer = vScrollBar.Value – dy
vScrollBar.Value = Math.Max(0, Math.Min(vScrollBar.Maximum – vScrollBar.LargeChange + 1, newVal))
End If
lastMousePosition = e.Location
‘ スクロールバーの値変更イベントにより、Invalidateが走るためここでの再呼び出しは不要
End If
End Sub
Protected Overrides Sub OnMouseUp(e As MouseEventArgs)
MyBase.OnMouseUp(e)
If e.Button = MouseButtons.Left Then
isDragging = False
Me.Cursor = Cursors.Default
End If
End Sub
”’
”’
Protected Overrides Sub OnPaint(e As PaintEventArgs)
If Me.bufferedGraphics Is Nothing Then Return
Dim g As Graphics = Me.bufferedGraphics.Graphics
‘ 1. 背景の高速クリア(アンバー色のダークテーマ風)
g.Clear(Color.FromArgb(20, 20, 23))
‘ 2. 描画品質の最適化設定
g.SmoothingMode = SmoothingMode.HighSpeed
g.PixelOffsetMode = PixelOffsetMode.HighSpeed
‘ 3. アフィン変換行列の設定(ワールド座標から物理座標への変換)
‘ X_d = X_w S – Offset_x
Dim offsetX As Single = hScrollBar.Value
Dim offsetY As Single = vScrollBar.Value
g.ResetTransform()
g.TranslateTransform(-offsetX, -offsetY)
g.ScaleTransform(zoomScale, zoomScale)
‘ 4. カリング用可視範囲の計算(仮想空間上における現在表示中の矩形)
Dim viewW_World As Single = (Me.Width – If(vScrollBar.Visible, vScrollBar.Width, 0)) / zoomScale
Dim viewH_World As Single = (Me.Height – If(hScrollBar.Visible, hScrollBar.Height, 0)) / zoomScale
Dim worldViewRect As New RectangleF(offsetX / zoomScale, offsetY / zoomScale, viewW_World, viewH_World)
‘ 5. 超高速カリング&レンダリングループ
‘ ※ペンオブジェクトの都度Newを徹底排除するため、色のハッシュマップか、
‘ もしくは動的に生成して即時Disposeする構造をとる。
For i As Integer = 0 To drawElements.Count – 1
Dim line As LineSegment = drawElements(i)
‘ 簡易境界ボックス判定による高速カリング(クリッピング)
Dim minX As Single = Math.Min(line.StartPoint.X, line.EndPoint.X)
Dim maxX As Single = Math.Max(line.StartPoint.X, line.EndPoint.X)
Dim minY As Single = Math.Min(line.StartPoint.Y, line.EndPoint.Y)
Dim maxY As Single = Math.Max(line.StartPoint.Y, line.EndPoint.Y)
If maxX < worldViewRect.Left OrElse minX > worldViewRect.Right OrElse
maxY < worldViewRect.Top OrElse minY > worldViewRect.Bottom Then
Continue For ‘ 画面外のオブジェクトは完全に無視
End If
‘ 必要なその瞬間にのみPenオブジェクトを生成し、完全に破棄する(GDIリークの防止)
‘ ※実業務では、色や太さごとにPenを数本キャッシュして再利用するのが最も高速。
Using p As New Pen(line.Color, 1.0F / zoomScale) ‘ ズーム時も線の太さを1物理ピクセルに保つ
g.DrawLine(p, line.StartPoint, line.EndPoint)
End Using
Next
‘ 6. デバッグ表示(物理座標系でオーバーレイ描画)
g.ResetTransform()
Using fontBrush As New SolidBrush(Color.LimeGreen)
Using debugFont As New Font(“Consolas”, 9.0F)
Dim visibleCount As Integer = 0 ‘ カリングされずに残った描画数(確認用)
g.DrawString($”Zoom: {zoomScale:F2}x”, debugFont, fontBrush, 10, 10)
g.DrawString($”Scroll X/Y: {hScrollBar.Value}/{vScrollBar.Value}”, debugFont, fontBrush, 10, 25)
g.DrawString($”Virtual Size: {virtualWidth}x{virtualHeight}”, debugFont, fontBrush, 10, 40)
End Using
End Using
‘ 7. バックバッファから画面(デバイス)への一括転送(BitBlt)
Me.bufferedGraphics.Render(e.Graphics)
End Sub
‘ コントロール破棄時のリソースクリーンアップ
Protected Overrides Sub Dispose(disposing As Boolean)
If disposing Then
If Me.bufferedGraphics IsNot Nothing Then
Me.bufferedGraphics.Dispose()
End If
If Me.bufferContext IsNot Nothing Then
‘ プロセスグローバルのため、通常はインスタンス単体でDisposeは不要だが、
‘ 参照カウンタを適切に維持するためnullクリアを行う
Me.bufferContext = Nothing
End If
End If
MyBase.Dispose(disposing)
End Sub
End Class
—
5. レガシー移行の知見:VB6 / VBA(Excel)への移植とWin32 APIの魔術
長年稼働しているVBAベースの社内システムや、VB6で開発されたデスクトップアプリを保守・近代化する際、上記のような「独自スクロールUI」を移植したいという要求は非常に多い。
しかし、VBA/VB6の `UserControl` や `PictureBox` には、.NETのような洗練された `BufferedGraphicsContext` や、標準の `HScrollBar` コントロールの動的ドッキング機能が貧弱、あるいは存在しない。このレガシー環境で同様のパフォーマンスを引き出すためには、Win32 APIを直接コールする「裏技」が必要不可欠となる。
Win32 GDI APIによるスクロールと描画の高速化
VB6/VBAにおいて、巨大な描画領域を低メモリでハンドリングする場合、以下のAPIを駆使して「OSレベルのスクロール支援」を受ける。
‘ スクロール領域をOSレベルで物理的にズラすAPI
Public Declare Function ScrollWindowEx Lib “user32” ( _
ByVal hWnd As Long, _
ByVal dx As Long, _
ByVal dy As Long, _
ByRef prcScroll As RECT, _
ByRef prcClip As RECT, _
ByVal hrgnUpdate As Long, _
ByRef prcUpdate As RECT, _
ByVal flags As Long) As Long
‘ デバイスコンテキストの原点をシフトするAPI(アフィン変換の代用)
Public Declare Function SetViewportOrgEx Lib “gdi32” ( _
ByVal hDC As Long, _
ByVal nX As Long, _
ByVal nY As Long, _
ByRef lpPoint As POINTAPI) As Long
① `SetViewportOrgEx` による描画オフセットの高速切り替え
.NETの `g.TranslateTransform` に相当する処理を、GDIレベルで行う。
描画関数群(`LineTo`, `BitBlt`)をコールする直前に、デバイスコンテキスト(hDC)の原点を `SetViewportOrgEx` でマイナス方向にズラすことで、描画コード自体は「仮想座標 $0, 0$」を基準としたままで、スクロール位置に応じた位置へOSが自動的にマッピングして描画する。
② `ScrollWindowEx` による再描画領域の極小化
ユーザーがスクロールバーを操作した際、画面全体をクリアして再描画するのは非効率極まりない。
`ScrollWindowEx` を呼ぶと、WindowsはグラフィックカードのVRAM上で、既に描画済みのピクセルデータを直接指定方向へビット転送(`BitBlt`)する。そして、「スクロールによって新たに画面に現れた領域(Update Region)」のみを無効化(Invalidate)し、そこだけの部分描画(部分Paint)をアプリケーションに要求する。これにより、描画負荷は数十分の一に低下する。
VBAにおけるGDIオブジェクトリークの悲劇と対策
VBAでAPIを用いたカスタム描画を行う際、最も頻発する不具合が「数時間立ち上げっぱなしにするとExcelが突然クラッシュする」現象である。これはGDIオブジェクトの解放漏れ(リーク)が原因だ。
VBA/VB6でペンやブラシを生成する場合、以下の黄金律を遵守しなければならない。
Dim hPen As Long, hOldPen As Long
‘ 1. ペンの作成
hPen = CreatePen(PS_SOLID, 1, vbRed)
‘ 2. デバイスコンテキスト(hDC)へ選択し、「古いペン」を必ず保存する
hOldPen = SelectObject(hDC, hPen)
‘ — ここで描画を行う —
MoveToEx hDC, 0, 0, ByVal 0&
LineTo hDC, 100, 100
‘ 3. 描画終了後、必ず「古いペン」をデバイスコンテキストに戻す(超重要)
Call SelectObject(hDC, hOldPen)
‘ 4. 作成したペンを「物理的に削除」してOSにメモリを返す(超重要)
Call DeleteObject(hPen)
この 「セレクトして、元に戻して、デリートする」 という3ステップの規律が1箇所でも崩れると、GDIハンドルはプロセス内に永久に残留する。VBAの開発環境(VBE)は、Excelと同一プロセス内で動作するため、デバッグ実行を停止(Endボタン押下)してもGDIオブジェクトは解放されず蓄積され続ける。
これが、レガシー環境での開発時に、定期的にExcel自体を再起動せねばならない最大の原因である。
—
6. まとめ:アーキテクトが語る「UI開発の設計思想」
標準コントロールの `AutoScroll` や、単純なピクチャボックスの配置は、プロトタイピングや小規模ツールには適している。しかし、数メガバイトのCADデータや、数ギガバイトに及ぶ地図タイルを扱うプロフェッショナルな業務システムにおいては、フレームワークの隠蔽した「ブラックボックス」を剥ぎ取り、描画の根本原理に立ち返る必要がある。
本稿で解説した、
- 仮想座標と物理座標の完全な二重構造化
- スクロールバー最大値仕様の補正設計
- GDI+における手動ダブルバッファリングと徹底的なカリング
- GDIオブジェクトの厳密なライフサイクル管理
これらは、.NET Frameworkがどれほど進化しようとも、あるいは将来的に別のUIテクノロジー(WPF、WinUI、あるいはWebベースのCanvas)へ移行しようとも通用する、コンピュータ・グラフィックスとユーザーインターフェース設計における不変の「真理」である。
レガシーシステムの保守から、次世代エンタープライズソリューションの設計まで、この極限の知見を役立てていただきたい。
