VB PictureBox と ShapeContainer について

タグの編集
投稿者 VB+VC# Beginner  (社会人) 投稿日時 2020/9/6 19:51:34
PictureBox と ShapeContainer についてお尋ねします。

Visual Basic で、アナログ時計を作る方法について、調べているうちに、
某書籍の抜粋のページを見つけ、その書籍を購入しました。
その書籍の説明では、
1.PictureBox をフォームに配置する。
2.Timer1.Tick のイベントで、時計盤(外周円、目盛、数字)を描画し、
 針(秒針・分針・時針)を描画する。
という流れでコードを書かれていました。
私はフォームの背景に写真を配置したので、

g.Clear(Me.BackColor)

の箇所を、

Pic_My_Analog_Clock.Refresh()

と書き換えたら、どうにか時計は動きました。
というより、「1秒ごとに時計を描画し、消してから再度描画する。」
という感じで、画面が点滅しています。

針を回転させる方法はないかとさらに調べていくと、
VisualBasic.PowerPacks をインストールし、LineShape の (x1,y1),(x2,y2)
の座標を計算すると、針は回転することがわかりました。
((x2,y2)を中心とするので (x1,y1) だけ変えればよい)
その方法で、針が回転することを確かめてから、
前述の PictureBox に時計盤(外周円、目盛、数字)を描画して、針を3本
LineShape で描画しましたが、PictureBox を一番背面にしても、フォームの
デザイン中に針は消え、「開始」を押しても、時計盤しか見えません。
(前述の「xxx.Refresh()」は今回は使っていません。
 時計盤の記述はフォームの load にしました。)
仕方がないので、PictureBox を消して、ShapeContainer に針、円、目盛を描画しました。

おそらく、私の知識不足で、PictureBox と ShapeContainer を同時に使うことが
できなかったと考えています。
アナログ時計のように、同心円の時計盤と針を描画する場合、
PictureBox と ShapeContainer を同時に重ねて利用することは可能ですか?
宜しくお願いします。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2020/9/7 00:30:09
> 某書籍の抜粋のページを見つけ、その書籍を購入しました。
何も書籍名を伏せずとも。(^^;


> 2.Timer1.Tick のイベントで、時計盤(外周円、目盛、数字)を描画し、
>  針(秒針・分針・時針)を描画する。
元の資料を見てみないと何とも言えませんが、
Tick 内で描画するのは、処理としては不自然ですね。

Tick の役目は、描画すべき座標計算値を更新したうえで、
PictureBox の Invalidate を呼ぶことだけに特化してみてください。
そして実際の描画処理は、PictureBox の Paint イベントで行わせるようにします。

また、Timer が並行して動作しているのなら、Refresh を呼ぶのは大げさで、
Invalidate で十分であるように思えます。これでちらつきが抑えられませんか?


> LineShape で描画しましたが、PictureBox を一番背面にしても、フォームの
> デザイン中に針は消え、「開始」を押しても、時計盤しか見えません。

仕様です。ざっくり言えば、描画されるレイヤーの違いです。
(それを制御しているのが、ShapeContainer という特殊コントロールなわけですが)

BackgroundImage などの描画結果は「下層」に描かれることになります。
LineShape / OvalShape / RectangleShape は、その上の「中層」に配置されます。
PictureBox や TextBox 等は、さらにその上の「上層」に配置されます。

各層はそれぞれ独立しており、層を超えた位置には配置できません。
たとえば、Form1 上に対して Graphics クラスの DrawLine メソッドを使った場合、
これは下層の描画処理なので、上層にある TextBox より手前に線が描かれることはありません。

同様に、中層にある各 Shape が、上層にある PictureBox や TextBox よりも
手前に来ることはありません。
ただし、コンテナコントロール(GroupBox や Panel 等)を経由すると、
それぞれのコンテナ内で、また下層・中層・上層とレイヤーが分かれることになります。

そして PictureBox はコンテナコントロールではないため、デザイン時には、
その上に他のコントロールを載せられないようになっています。

やろうと思えば、実行時に、「Me.PictureBox1.Controls.Add(Me.ShapeContainer1)」などとして
配置することはできなくもないですし、.desiger.vb を無理矢理書き換えれば
それっぽくなりますが…副作用が怖いので、自分なら Shape に頼らず、自前で描画してしまいますね。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2020/9/7 10:04:18
> PictureBox の Invalidate を呼ぶことだけに特化してみてください。
> そして実際の描画処理は、PictureBox の Paint イベントで行わせるようにします。

Invalidate メソッド + Paint イベントを使ったアナログ時計の
具体的なサンプルがあったので紹介しておきますね。
http://hanatyan.sakura.ne.jp/patio/read.cgi?no=235

このサンプルでは、背景画像を貼ったとしても、ちらつきが気になることはありませんでしたが、
ちらつきを抑えるために DoubleBuffered プロパティを使っている点が大きいです。
(DoubleBuffered を False にするとちらつきます)
ただしこれは PictureBox ではなく Form に直接描画しているためにおこることです。
PictureBox に描画する場合は最初からダブルバッファリングが有効なので、気にしなくて OK。

ダブルバッファリングの恩恵にあずかるためには、
描画処理を Paint イベントに集約する必要があります。



上記サンプルでは、1 秒ごとに秒針がすすむ時計となっていますが、
1 秒未満も連続的に進めるようにしたい場合は、Timer1.Interval を小さくしたうえで、
Timer1 の Tick イベント内の条件判定を捨てて、『Me.Invalidate()』だけにすれば OK。

あとは秒針のための計算処理を
 Dim secAng As Double = 2.0 * pai * time.Second / 60.0
から
 Dim secAng As Double = 2.0 * pai * (time.Second + time.Millisecond / 1000.0) / 60.0
にすれば、秒針をより細かく動かすことができます。

ついでに、Paint イベントの所で、
 Dim g As Graphics = e.Graphics
の下に
 g.SmoothingMode = SmoothingMode.HighQuality
を入れておくと、描画結果が綺麗になると思います。
投稿者 VB+VC# Beginner  (社会人) 投稿日時 2020/9/7 19:47:09
魔界の仮面弁士様

2度にわたり、(+前回の質問と合わせると3回も)
ご丁寧な説明をしていただき、ありがとうございました。

書籍は
「例題でわかるVisual Basic .NET」 東京電機大学出版局
です。
2、3か月前にはアナログ時計に関する抜粋がネット上にありましたが、見失いました。
また、Invalidate メソッドのページに到着しながら、使い方がわからず、スルーしていました。

Visual Basic にレイヤーがあることを初めて知りました。
CADをかじったことがあるので、レイヤーの説明はおぼろげながら理解しました。

ご丁寧な説明とリンクのサンプルはちらっと見ただけで、まだ十分に理解していませんが、
ぼちぼち取り組んでみます。

30年以上前に学校で、FORTRAN, COBOL を習って以降、
ほとんどの年月は EXCEL+VBA 以外を使うことがありませんでした。
10年ほど前から、作りたいものが見つかったら、VB と VC# に挑戦していますが、
独習で、なかなか思うようにできません。

また質問するかと思いますが、その時もよろしくお願いいたします。

最後にもう一つ質問します。
ShapeContainer を利用する場合、
私は Timer1.Tick の中に
1.テキストボックスに「日時+曜日+時刻」を表示する
2.1.のテキストボックスの「曜日」によって、文字の色を変える
3.アナログ時計の針の座標を計算する(現在時刻を表示する)
を入れています。テキストボックスの表示をそこに入れたのは、
実行中に日付が変わることを考慮したかったからです。
この場合も、 Timer1.Tick の中でこれらを処理するのは不自然ですか?



投稿者 魔界の仮面弁士  (社会人) 投稿日時 2020/9/7 22:10:58
> 「例題でわかるVisual Basic .NET」 東京電機大学出版局

(母校だ…!)


読んだことは無いですが、2004年出版の物らしいので、流石に内容が古すぎる気がします。
読まずに批評したくはないですが……15年以上前のものですよね。
「ジェネリック」や「WFP」が登場する前の代物ですし、個人的にはお奨めしかねます。

VB には何度か大きな技術変革がありますが、その最初の波が 2005 年でした。

目次を見る限りでは、本当に基礎の基礎部分のみの内容ではありそうなので、
バージョン依存性がそこまで激しいわけでは無いと思いますが、ちょっと心配です。
たとえば、当時使われていた ArrayList コレクションなどは、現在は推奨されなくなっていますし、
今と当時とでは、For ループの書き方一つとっても微妙に異なってきます。


> Visual Basic にレイヤーがあることを初めて知りました。
説明のためにレイヤーという言葉を使いはしましたが、そういう表現が、
VB のマニュアル上に出てくるわけではないですけれどね。


> CADをかじったことがあるので、レイヤーの説明はおぼろげながら理解しました。
Graphics クラスでの「描画処理」だけであれば、レイヤー的な表現を実装することは可能です。

しかし、既存のコントロールの上に覆いかぶさるような描画処理を
Windows Forms で実装することは現実的ではありません。


> ShapeContainer を利用する場合、
デザイン時に配置できるのでお手軽ではありますが、単に描画目的だけであれば、
直接 Graphics クラスで描画した方が融通が利くことも多いです。

ただし、各シェイプは実行時にマウスで選択可能となっていますので、
単に描画するだけではなく、選択可能な描画オブジェクトとして画面に配置するような
目的では便利だと思います。


> この場合も、 Timer1.Tick の中でこれらを処理するのは不自然ですか?
Graphics を操作するのは不自然な実装になりがちですが、
TextBox を書き換えるのは、さほど不自然では無いですね。

(たとえば、Timer 内で CreateGraphics メソッドを使ったりするのは不自然です)


> 私は Timer1.Tick の中に
> 1.テキストボックスに「日時+曜日+時刻」を表示する
Tick イベントでも Click イベントでもそうですが、イベント内で処理した結果は
画面に直ちに反映されるわけではありません。

イベント内で Label や TextBox 等の Text プロパティを書き換えたとしても、
イベント処理中はビジー状態となるため、画面には即座に反映されません。
イベント処理が終わってアイドリング状態になった時にはじめて、画面に反映されます。


画面への描画処理は、Tick イベントとは別のタイミング(Paint イベント等)で
発生するようにできています。それ以外のタイミングで PictureBox を
不用意に再描画すれば、ちらつきを生みやすくなります。


PictureBox への描画処理について話すならば、Invalidate / Update / Refresh メソッドの
違いまで語りたいところですが、いったんここまで。
投稿者 VB+VC# Beginner  (社会人) 投稿日時 2020/10/12 21:59:27
 魔界の仮面弁士様
9/7に返答して頂いていましたことに、2・3日前に初めて気づき、
お礼が遅れましたこと、大変失礼致しました。
前回、
Private Sub Timer_Tick(sender As Object, e As EventArgs)
    PictureBox.Invalidate()
end Sub

Private Sub PictureBox_Paint(sender As Object, e As PaintEventArgs)
    Dim g As Graphics = e.Graphics
    g.SmoothingMode = SmoothingMode.HighQuality
    ....
end Sub

を試して、ようやく正常に動作することができました。

Public g As Graphics
Private Sub Timer_Tick(sender As Object, e As EventArgs)
    PictureBox.Refresh()
    g = PictureBox.CreateGraphics()
    g.SmoothingMode = SmoothingMode.HighQuality

end Sub

と記述していたので、『 Refresh() 』を『 Invalidate() 』に変えても、
直らず、『 Public g As Graphics 』 と宣言していたため、
『 Dim g As Graphics = e.Graphics 』と記述することができなかったのが原因でした。

その間、コードの中で、
Button
Label()
PictureBox
Timer
ShapeContainer
LineShape()
等を作ることができることを知り、
(ShapeContainer と LineShape の作り方を調べているとき、魔界の仮面弁士様の過去の投稿を
 発見し、大変ヒントになりました。)
自分の中では、
「フォームエディターでは、Form のみ定義し、後はコードの中で定義するのが一番作りやすい」
と思うようになってきました。
また、アナログ時計に関しては、
① Graphics で表示するのは、非常に簡単ではあるが、私には、文字盤
  (外周円と目盛と時間を示す数字)は最初に一度だけ書いて、時計の3つの針だけを
  書き直せばいいのではないか?
② おまけに、前述の間違った方法のせいで、画面がパカパカ点滅する。
  (これは解決しました。)
③ 面倒ではあるが、LineShape() で目盛を用意しておいて、3つの針の開始点( StartPoint )のみを
  書き直せば針は回転する。
  LineShape(i) として利用できるので、私は大変重宝しています。 
という考えを持っています。
そこで、新たな質問です。

④ 『 Dim g As Graphics = e.Graphics 』はペイントイベントで書きますが、ペイントイベントの中に
  サブルーチンを作って、『 g 』を継承することはできませんか?
  私のテストでは、実現できませんでした。
⑤  Graphics で記述する方法で、
  文字盤のところは一度書いたら、そのままにしておき、中の3本の針(+中心点)だけを
  書き換えることはできますか?
  私の Form の ClientSize と PictureBox は正方形なので、
    例えば、正方形の日の丸の中央の円の中だけを書き換えたいのです。
⑥ 『 Invalidate / Update / Refresh メソッド 』については、全くわかりません。
  https://dobon.net/vb/dotnet/control/refreshupdateinvalidate.html
  様の記述を読み、テスト用の実行ファイルを使っても理解できません。

魔界の仮面弁士様の説明は詳しくて丁寧なので、また追加質問をしてしまいました。
宜しくお願い致します。





投稿者 魔界の仮面弁士  (社会人) 投稿日時 2020/10/13 11:36:28
> Public g As Graphics
> と記述していたので、

インスタンスの生存期間を考えると、
Graphics をフィールド変数として維持するのは都合が悪いですね。


>『 Public g As Graphics 』 と宣言していたため、
>『 Dim g As Graphics = e.Graphics 』と記述することができなかったのが原因でした。

良し悪しは別として、フィールド変数として g が宣言されていても、
同じ名前でローカル変数 g を多重宣言することはできるはずですよ。

ローカル変数とフィールド変数で名前が競合していた場合、
Me.g と書けばフィールド変数に、g と書けばローカル変数にアクセスできます。



> ① Graphics で表示するのは、非常に簡単ではあるが、私には、文字盤
>   (外周円と目盛と時間を示す数字)は最初に一度だけ書いて、時計の3つの針だけを
>   書き直せばいいのではないか?

固定サイズのフォームなら、時計盤をあらわす .png 画像を用意しておいて、
それをデザイン時に背景画像に割り当てておくのがお手軽です。

ファイルとして事前に用意するかわりに、実行時にメモリ上に画像を動的に構築し、
その画像に対して Graphics クラスを利用して描画するという方法も使えます。


> ④ 『 Dim g As Graphics = e.Graphics 』はペイントイベントで書きますが、

書かなくても使えますけれどね。処理としては
 e.Graphics.DrawLine(…)
と一行で書くか、
 Dim g = e.Graphics
 g.DrawLine(…)
と書くかの違いでしかないわけで。

PictureBox1.CreateGraphics() は「メソッド」なので、
呼び出すたびに、新しい Graphics が生成されてしまいます。
そのため、生成されたインスタンスを変数に受けて、それを使う必要がありました。

一方、e.Graphics は「プロパティ」であり、同じイベント内であれば、
何度呼び出しても、同じ Graphics インタンスを得る事ができます。
そのためこちらは、変数に受けずに使うこともできます。


> ペイントイベントの中にサブルーチンを作って、

Visual Basic が .NET 対応になる前は、GoSub ステートメントというものを通じて
【サブルーチン】を呼び出せるようになっていたのですが、現在の Visual Basic では
【サブルーチン】がサポートされていません。

【サブルーチン】の代わりに、「Sub プロシージャー」や「Function プロシージャー」を使ってみてください。
(文意によってはプロシージャーという言葉の代わりに、メソッドやイベントハンドラという名で呼ばれることもあります)

Private Sub PictureBox1_Paint(sender As Object, e As PaintEventArgs) Handles PictureBox1.Paint
    '自作の Sub プロシージャーを呼び出して、時計の針を描く 
    DrawHands(e.Graphics, Now)

    '実際には、Graphics だけだと領域サイズが不明瞭なので、 
    '描画範囲を示す Rectangle も渡した方が良いかもしれません。 
    '(e.ClipRectangle とか、PictureBox1.ClientRectangle とか) 

End Sub

Private Sub DrawHands(g As Graphics, tm As Date)
    '時分秒を整数で得たい場合 
    Dim intH = tm.Hour
    Dim intM = tm.Minute
    Dim intS = tm.Second

    '時分秒を小数点以下も含めて得たい場合 
    Dim span = tm - tm.Date
    Dim h = span.TotalHours
    Dim m = span.TotalMinutes - (span.Hours * 60)
    Dim s = span.Seconds + (span.Milliseconds / 1000.0F)

    DrawHourHand(g, h)
    DrawMinuteHand(g, m)
    DrawSecondHand(g, s)
End Sub

Private Sub DrawHourHand(g As Graphics, h As Single)
End Sub
Private Sub DrawMinuteHand(g As Graphics, m As Single)
End Sub
Private Sub DrawSecondHand(g As Graphics, s As Single)
End Sub


プロシージャ内で局所的に使いたいルーチンの場合には、
メソッドとして切り出すかわりに、ラムダ式というものを使うこともできます。
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2020/10/13 13:11:01
> 『 g 』を継承することはできませんか?
> 私のテストでは、実現できませんでした。

ごめんなさい。質問の意味が分かりませんでした。
Visual Basic における継承とは、Inherits キーワードのことを指しますが、
それとは違う話でしょうか。プログラミングでいうところの「委譲」の話ですか?


継承ということであれば、PictureBox を Inherits して、
OnPaint メソッドを Overrides すれば、Form 側で Paint イベントを実装せずとも
Graphics 処理を継承先クラスに一任できますが…ややこしくなるので説明は省略して、
OnPaint を使った描画サンプルだけ置いておきます。
https://www.atmarkit.co.jp/fdotnet/practprog/wisearch02/wisearch02_03.html


継承せずに Graphics を処理させる方法としては、
先のように、Graphics を引数にとるプロシージャーを用意して、
それを呼び出すことができます。それでは要件を満たせないでしょうか?


> ⑤  Graphics で記述する方法で、
> 文字盤のところは一度書いたら、そのままにしておき、中の3本の針(+中心点)だけを
> 書き換えることはできますか?

いわゆるレイヤーの概念ですね。

標準では、下記の 3 機能をレイヤーとして使えます。
 (1) PictureBox1.BackgroundImage の背景画像
 (2) PictureBox1.Image の前景画像
 (3) PictureBox1.Paint イベントでの e.Graphics
※このほか、PictureBox1.CreateGraphics() による描画層があります。

時計の針と違って、文字盤は変化しませんので、背景画像として保持しておくのがお奨めです。
Image 系プロパティに渡す画像は、開発時に事前に用意しておいても良いですし、
下記のように実行時に動的に生成しても良いでしょう。

'  Imports System.Drawing 
'  Imports System.Drawing.Drawing2D 

Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    Dim size = PictureBox1.Size
    PictureBox1.BackgroundImageLayout = ImageLayout.None
    PictureBox1.BackgroundImage = CreateClockFace(Math.Min(size.Width, size.Height) \ 2)
End Sub

Private Function CreateClockFace(radius As IntegerAs Image
    Dim rect As New Rectangle(0, 0, radius * 2, radius * 2)
    Dim faceRect = Rectangle.Inflate(rect, -5, -5)
    Dim bmp As New Bitmap(rect.Width, rect.Height)
    Using g = Graphics.FromImage(bmp)
        g.Clear(Color.Transparent)
        g.PixelOffsetMode = PixelOffsetMode.HighQuality
        g.CompositingQuality = CompositingQuality.HighQuality

        Using gb As New LinearGradientBrush(rect, Color.LightGreen, Color.Yellow, LinearGradientMode.ForwardDiagonal)
            g.FillEllipse(gb, faceRect)
        End Using
        Using p As New Pen(Brushes.Blue, 5.0F)
            g.DrawEllipse(p, faceRect)
        End Using
        Dim r = faceRect.Width / 2.0F
        For h = 1 To 12
            Dim x = r * CSng(Math.Cos(Math.PI * (h - 1) / 6.0F)) + r + 2.5F
            Dim y = r * CSng(Math.Sin(Math.PI * (h - 1) / 6.0F)) + r + 2.5F
            g.FillEllipse(Brushes.Yellow, x, y, 4.0F, 4.0F)
        Next
    End Using
    Return bmp
End Function



この方法は、レイヤー数をもっと増やしたい場合にも応用できます。

上記のように、背景透過な Bitmap インスタンスを動的に生成しておくと、
あとからそれを、Graphics クラスの DrawImage何某 系メソッドで描画できるので、
BackgroundImage 等を使わずとも、任意の枚数の画像を重ね合わせて表現できます。


> 私の Form の ClientSize と PictureBox は正方形なので、

もしも PictureBox を矩形(正方形/長方形)以外の形状にしたい場合には、
Region プロパティを使うことができます。
https://dobon.net/vb/dotnet/form/formregion.html
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2020/10/13 13:56:46
> ⑥ 『 Invalidate / Update / Refresh メソッド 』については、全くわかりません。
>   https://dobon.net/vb/dotnet/control/refreshupdateinvalidate.html
>   様の記述を読み、テスト用の実行ファイルを使っても理解できません。

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    For n = 1 To 1000
        Label1.Text = n.ToString()
    Next
End Sub


たとえば、上記のコードを実行した場合、Label1 に表示されるのは
最後の 1000 という文字列だけであり、途中経過は表示されませんよね。

ループ回数を増やしたとしても、処理中はアプリケーションが固まるだけであり、
結局実際に描画されるのは、最後の結果のみとなります。


これは、イベント処理中というのはいわゆる「ビジー状態」に陥っており、
その作業中は、画面の再描画は何も行われないためです。

処理結果が実際に画面に反映されるのは、イベント処理が終わった後の、
何も処理されていない「アイドル状態の時」に行われる仕様です。



さてここで、Refresh メソッドを呼んでみましょう。

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    For n = 1 To 1000
        Label1.Text = n.ToString()
        Label1.Refresh()  '❄強制再描画 
    Next
End Sub


Refresh メソッドを呼び出すと、ビジー状態であっても割り込みで描画処理が行われます。
上記のコードだと、1000 に至るまでの途中経過もしっかり描画されることを確認出来るでしょう。

しかしその分、ループを抜けるまでのトータル時間が遅くなっていることも体感できるかと思います。


つまり描画処理とは、相対的には「遅い」処理であると言えます。
そのため、ゲームやアニメのように、高頻度に画面を書き換える必要がある場合は別として、
ほとんどのアプリケーションでは、頻繁に画面を書き換えるべきではありません。


更新頻度が高すぎれば、どうせ人間の目では視認しきれないわけなので、
適当に間引いて更新頻度を抑えれば、速度の低下を抑えたまま途中経過の表示を実現できます。

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    For n = 1 To 1000
        Label1.Text = n.ToString()
        If n Mod 25 = 0 Then
            Label1.Refresh()
        End If
    Next
End Sub




さて今度は、何も変更していないのに、Refresh を呼んでみましょう。
今回は所要時間の測定も行ってみます。

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    Dim sw = Stopwatch.StartNew()
    For n = 1 To 1000
        Label1.Refresh()
    Next
    sw.Stop()
    MsgBox(sw.ElapsedMilliseconds)
End Sub



この処理は、私の環境ではおよそ 400ミリ秒かかっていました。
しかし、Label1.Refresh() を Label1.Update() に変更した場合は 0 ミリ秒です。
Invalidate メソッドの場合は、数ミリ秒程度でした。



Update というのは、「再描画が必要なら描き換えるが、不要なら何もしない」メソッドなので、
Label の内容に差異が無ければ、実行時間はほぼゼロとなります。

Invalidate は、そのコントロールを「再描画が必要な状態」としてマークするためのものです。
再描画が終わった後は、そのコントロールが「再描画が不要な状態」に戻ります。

Refresh メソッドは、「Invalidate と Update を連続で呼び出すだけ」の強制再描画メソッドです。


⚡【Paint イベント】
 画面サイズの変更や、上に重なっていた別のウィンドウが取り除かれた場合など、
 「再描画」が必要な時に呼び出されます。
 独自の描画処理を行いたい場合は、ここの e.Graphics を利用して実装します。
 e.Graphics への描画処理結果は、このイベントの処理中は画面に反映されませんが、
 このイベント終了が終わった後のアイドル時に画面に反映されます。


🔸【CreateGraphics メソッド】
 CreateGraphics で生成した Graphics への描画結果は、直ちに画面上に反映されます。
 線を 1 本描くだけでも直ちに反映されてしまうため、描画途中の結果も
 随時表示されてしまい、ちらつきが強くなりがちです。
 また、CreateGraphics で描画した結果は、その上に他のウィンドウが重なるなどすると
 容易に消えてしまいます。
 通常は CreateGraphics ではなく、Paint イベントの e.Graphics を使うようにしましょう。


🔹【Invalidate メソッド】
 コントロールに対して、描画処理が必要であるという無効化マークを付与するための物です。
 言い換えると、再描画を「依頼」するためのメソッドであると言えるでしょう。
 これを呼んだとしても、直ちに何かが起こるわけではありませんが、
 これを呼んでおくことで、アイドル状態になったときに Paint イベントが誘発されます。
 なお、Invalidate を複数回連続で呼んだとしても、Paint の発生はアイドル時の一回のみです。


🔹【Update メソッド】
 再描画が必要とされていた場合に、Paint イベントを直ちに呼び出します。
 再描画の必要が無い場合には、呼び出しても何も起きません。


🔹【Refresh メソッド】
 いわゆる「強制再描画」メソッドです。
 このメソッドの呼び出しは、Invalidate メソッドと Update メソッドを連続で呼び出すことに等しいです。
 再描画が必要であるとマークされていようといまいと、強制的に再描画されることになるので、
 不用意に呼びすぎると、処理の低速化を招きます。
投稿者 VB+VC# Beginner  (社会人) 投稿日時 2020/10/13 19:33:37
魔界の仮面弁士様

長文の書き込み、大変ありがとうございました。
お時間を割いて頂き、申し訳ありませんでした。
軽く流し読みしただけなので、この後時間をかけて解読してみます。

私が前回、「サブルーチン」と呼んだのは、
「Private sub xxxx() ... end Sub」
のことでした。グラフィックを引数にすればいいのですね。

Form の変形について、紹介して頂いたので、
PictureBox の変形を試してみようと思っています。

書いて頂いたことを試してみて、わからないことをまた質問すると思います。

文字盤については、画像を利用することをネットで知りましたが、
いまだに「自分でコードで書く!」という願望があるので、試行錯誤してみます。

わからないことは結構ネットで調べますが、それでもわからないことは
「実現不可能かな?」と諦めていました。
でも、質問すれば、親切な回答が返ってくるので、この掲示板を利用し始めて
本当に良かったと思っています。

まずはお礼まで。



投稿者 VB+VC# Beginner  (社会人) 投稿日時 2020/10/15 23:59:40
魔界の仮面弁士様

こんばんは。
先日書きこんでいただいた内容をゆっくり読んでみました。

『 Invalidate / Update / Refresh メソッド 』について、
実験結果からある程度分かってきました。
ありがとうございます。

『 もしも PictureBox を矩形(正方形/長方形)以外の形状にしたい場合には、
Region プロパティを使うことができます。
https://dobon.net/vb/dotnet/form/formregion.html 』

とても参考になりました。
① PictureBox を2つ
  (もう一度「日の丸」を例にしますと、外側の白い部分と内側の赤い円の部分)
  に分割することによって、外側の目盛等は1回だけ、内側の針と中央の点は毎秒
  描画することができました。
② 今回の質問の一番最初に、「 PictureBox を作ると針が消えてしまう 」問題も、
  PictureBox に円をくりぬくことによって、解決しました。

上記の変更を試行錯誤しながら行っているうちに、新たな疑問が出てきました。
その説明の前に、私の時計の具体的な大きさを書きますと、
フォームのクライアントサイズは「 1000 x 1000 」
原点の位置を変更する方法を知らない時に作り始めましたので、原点は左上で
時計の中心の座標は (500,500) です。
数字や目盛は全て半径 400 以上、針は半径 350 以下なので、今回の内側の領域は
半径 375 つまり、「 750 x 750 」です。

①の場合、

    path.AddEllipse(New Rectangle(500 - 375, 500 - 375, 375 * 2, 375 * 2))

の円をくりぬきましたので、内側に配置する針と点は、元の中心点 (500,500) ではなく、
新しい領域の中心点 (375,375) を基準にして描画しないと、フォームの中心に
描画されませんでした。その時は、「円の内側の領域は領域の左上が 125 ずれた分、
中心がずれたのかな」と理解しました。

ところが、②の場合、ShapeContainer の領域を

    .SetBounds(500 - 375, 500 - 375, 375 * 2, 375 * 2)

    .SetBounds(0, 0, 1000, 1000)

のどちらにしても、針の EndPoint ゃ中央の点の座標は (500,500) にしないと、
正しく描画されません。
ということは、PictureBox に円をくりぬきさえすれば、ShapeContainer の
位置やサイズは変更する必要がないのでしょうか?

取り敢えず、①・②とも、希望通りに動いてくれているので問題ないのですが、
記述方法が異なる理由が知りたくて、質問させて頂きました。
説明が拙くて、わかりづらくなってすみません。