PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : [VB .NET] Dynamische(s) Formular-Seitenverhältnis & -Maximalgröße


dariegel
2008-08-03, 12:56:24
Hallo zusammen,

ich programmiere derzeit einen Tetris-Klon (Bild siehe unten). Das Spiel läuft sehr gut soweit, allerdings bereitet mir die dynamische Größenänderung Kopfzerbrechen. Die Blockgröße des Spielfeldes und der Vorschau auf den nächsten Spielstein soll sich dynamisch an die Formulargröße anpassen, kurzum: mein Tetris soll komplett skalierbar sein.
Dazu fange ich per Subclassing die WM_SIZING- und die WM_GETMINMAXINFO-Nachricht ab. Erstere dient zur Festlegung des Formular-Seitverhältnisses, letztere zur ensprechenden Anpassung der Maximalgröße des Formulars. Die statische Variable dblAspectRatio ändert sich dynamisch, sie ist das Verhältnis aus Höhe und Breite von (Spielfeld + Vorschau).

Der Code funktioniert soweit auch gut, allerdings flackerte die Formulargröße zunächst bei Erreichen der Maximalgröße um +/-1. Der Grund dafür war, dass in einer früheren Entwicklungsstufe die Variable dblAspectRatio noch für beide Nachrichten separat errechnet wurde, eine Nachricht hatte also ein aktuelleres Seitenverhältnis zur Verfügung als die andere. Da beide aber fast zeitgleich ausgeführt werden und die Maximalgröße über das aktuelle Seitenverhältnis errechnet wird, musste ich sicherstellen, dass beiden Nachrichten dasselbe dblAspectRatio zur Verfügung steht. Durch Debuggen fand ich heraus, dass WM_GETMINMAXINFO vor WM_SIZING ausgelöst wird, deshalb wird dblAspectRatio in diesem Codeblock (WM_GETMINMAXINFO) aktualisiert und gespeichert, um dann auch für die WM_SIZING-Nachricht zur Verfügung zu stehen. Diesen Code seht Ihr unten.

Entschuldigt, dass ich vor meiner eigentlichen Problembeschreibung so weit aushole, ich möchte Euch nur klarmachen, warum der Code so ist, wie er ist.



Protected Overrides Sub WndProc(ByRef msg As System.Windows.Forms.Message)
Dim dblTitlebarHeight As Double = Me.Height - Me.ClientSize.Height 'Höhe der Titelleiste.
Dim dblBorderWidth As Double = Me.Width - Me.ClientSize.Width 'Rahmendicke links & rechts.

Const WM_SIZING As Long = &H214
Const WM_GETMINMAXINFO As Long = &H24

Const WMSZ_LEFT As Integer = 1
Const WMSZ_RIGHT As Integer = 2
Const WMSZ_TOP As Integer = 3
Const WMSZ_TOPLEFT As Integer = 4
Const WMSZ_TOPRIGHT As Integer = 5
Const WMSZ_BOTTOM As Integer = 6
Const WMSZ_BOTTOMLEFT As Integer = 7
Const WMSZ_BOTTOMRIGHT As Integer = 8

Static dblAspectRatio As Double


'Gilt die Nachricht diesem Formular?
If msg.HWnd.Equals(Me.Handle) Then
Select Case msg.Msg
Case WM_SIZING 'Wird nach WM_GETMINMAXINFO aufgerufen.
'lParam-Feld des Message-Objekts in Rect-Struktur umwandeln.
Dim wndRect As Rect = DirectCast(Marshal.PtrToStructure(msg.LParam, wndRect.GetType), Rect)

'Aktuelle Formularbreite und -höhe speichern.
Dim dblWid As Double = wndRect.right - wndRect.left
Dim dblHgt As Double = wndRect.bottom - wndRect.top

If msg.WParam.ToInt32 = WMSZ_LEFT Or msg.WParam.ToInt32 = WMSZ_RIGHT Then
dblHgt = dblWid * dblAspectRatio
wndRect.left = wndRect.right - CInt(dblWid)
ElseIf msg.WParam.ToInt32 = WMSZ_TOP Or msg.WParam.ToInt32 = WMSZ_BOTTOM Then
dblWid = dblHgt / dblAspectRatio
wndRect.top = wndRect.bottom - CInt(dblHgt)
ElseIf msg.WParam.ToInt32 = WMSZ_TOPLEFT Or msg.WParam.ToInt32 = WMSZ_TOPRIGHT Then
dblHgt = dblWid * dblAspectRatio
wndRect.top = wndRect.bottom - CInt(dblHgt)
ElseIf msg.WParam.ToInt32 = WMSZ_BOTTOMLEFT Or msg.WParam.ToInt32 = WMSZ_BOTTOMRIGHT Then
dblHgt = dblWid * dblAspectRatio
wndRect.bottom = wndRect.top + CInt(dblHgt)
End If

wndRect.bottom = wndRect.top + CInt(dblHgt)
wndRect.right = wndRect.left + CInt(dblWid)

'lParam-Feld des Message-Objekts aktualisieren.
Marshal.StructureToPtr(wndRect, msg.LParam, True)

'0 als Return Code an Windows zurückgeben (lt. MSDN).
msg.Result = New System.IntPtr()


Case WM_GETMINMAXINFO 'Wird vor WM_SIZING aufgerufen.
'lParam-Feld des Message-Objekts in MinMaxInfo-Struktur umwandeln.
Dim mmiStruct As MinMaxInfo = CType(Marshal.PtrToStructure(msg.LParam, mmiStruct.GetType), MinMaxInfo)

'Aktuelles Aspektverhältnis speichern.
dblAspectRatio = (2 * picGameBoard.Left + Toolbar.Height + Statusbar.Height + picGameBoard.Height + dblTitlebarHeight) / (3 * picGameBoard.Left + picGameBoard.Width + picPreview.Width + dblBorderWidth)

'Minimale Fenstergröße festlegen.
mmiStruct.ptMinTrackSize.x = 200
mmiStruct.ptMinTrackSize.y = CInt(mmiStruct.ptMinTrackSize.x * dblAspectRatio)

Debug.WriteLine("Min. Size: " & mmiStruct.ptMinTrackSize.x.ToString & "x" & mmiStruct.ptMinTrackSize.y.ToString)

'Maximale Fenstergröße festlegen.
mmiStruct.ptMaxTrackSize.y = Screen.PrimaryScreen.WorkingArea.Height
mmiStruct.ptMaxTrackSize.x = CInt(mmiStruct.ptMaxTrackSize.y / dblAspectRatio)

Debug.WriteLine("Max. Size: " & mmiStruct.ptMaxTrackSize.x.ToString & "x" & mmiStruct.ptMaxTrackSize.y.ToString)

'lParam-Feld des Message-Objekts aktualisieren.
Marshal.StructureToPtr(mmiStruct, msg.LParam, True)

'0 als Return Code an Windows zurückgeben (lt. MSDN).
msg.Result = New System.IntPtr()


End Select
End If

MyBase.WndProc(msg)
End Sub



Nun zu meinem eigentlichen Problem: starte ich die Anwendung und ziehe eine Seite (egal welche) des Fensters größer, so bleibt das Fenster kurz unterhalb der Maximalgröße stehen. Bei mir beträgt die vertikale Maximalgröße (mmiStruct.ptMaxTrackSize.y) 800 Pixel, das Fenster bleibt bei ca. 790 Pixel stecken. Setzte ich dann mit der Maus ab und ziehe das Fenster erneut, kann ich mich langsam an die 800 Pixel herantasten. Durch Debuggen fand ich heraus, dass mmiStruct.ptMaxTrackSize.x zu spät aktualisiert wird, deshalb auch das Steckenbleiben bereits unterhalb der tatsächlichen Maximalgrenze.

Nun war meine Idee, den Code aus dem Block WM_GETMINMAXINFO mit der Nachricht WM_SIZING ausführen zu lassen. Nur geht das natürlich nicht. Ich kann zwar msg.LParam nacheinander sowohl in Rect als auch in MinMaxInfo "casten", zurückschreiben (Marshal.StructureToPtr(mmiStruct, msg.LParam, True)) kann ich allerdings nur eine Struktur.


Ich würde diese beiden Nachrichtenbehandlungen gern effizienter behandeln, bzw. ihr Zusammenspiel besser steuern können. Denn der Code aus beiden ist ja durch dblAspectRatio voneinander abhängig.

Habt Ihr Vorschläge, wie ich das Problem mit der Maximalgröße lösen kann? Über Vorschläge wäre ich sehr dankbar!


Vielen Dank für's Lesen!


Gruß,
Alex


Hier schön zu sehen: das sich mit der Spielfeld-/Vorschaugröße ändernde Verhältnis.

http://home.arcor.de/alexriegel/tetris.png http://home.arcor.de/alexriegel/tetris2.png


BTW: Warum lässt eigentlich vb@rchiv nur Beiträge <= 5 KB durch? :mad:

Der_Donnervogel
2008-08-03, 15:27:55
Habt Ihr Vorschläge, wie ich das Problem mit der Maximalgröße lösen kann? Über Vorschläge wäre ich sehr dankbar!Also erst mal stellt sich bei mir die Frage, wozu man eine Maximalgröße einstellt, wenn sowieso alles skalierbar ist.

Was ich mich auch frage ist, was es für einen Grund hat warum das ganze so kompliziert implementiert ist. VB.Net bietet doch alle relevanten Events in der Formklasse an. Mit dem Event "Resize" kann man die Größenänderung abfangen und mit der Form-Property "MaximumSize" kann man die Maximalgröße problemlos einstellen und mittels der Properties Width und Height kann man die Größe des Forms auslesen. Dabei passiert auch nicht das Problem, dass man schon bei 790 Pixeln hängen bleibt. Auf dem Form malen kann man auch, wenn man sich ein Graphics-Objekt mit Me.CreateGraphics() macht. Die Rectangle-Objekte die man da (bei einem Tetris) braucht, haben sogar out-of-the-box Funktionen wie IntersectsWith, man kann also auch sehr einfach auf Kollisionen prüfen. Wozu also der ganze Stunt mit den WM-Messages? :confused:

RattuS
2008-08-03, 16:35:47
Da versucht gerade jemand das Rad neu zu erfinden. Alle deine benötigten Ereignisse und Funktionen gibt es bereits im .NET. Du brauchst dafür nicht eine einzige Win32-API manuell benutzen.

dariegel
2008-08-03, 18:31:02
Zunächst danke für Eure Antworten.

Also erst mal stellt sich bei mir die Frage, wozu man eine Maximalgröße einstellt, wenn sowieso alles skalierbar ist.

Ich lege deshalb ein Maximum fest, da das Fenster nicht größer als der Bildschirm werden soll. Anonsten gibt es keinen Grund.


Was ich mich auch frage ist, was es für einen Grund hat warum das ganze so kompliziert implementiert ist. VB.Net bietet doch alle relevanten Events in der Formklasse an. Mit dem Event "Resize" kann man die Größenänderung abfangen und mit der Form-Property "MaximumSize" kann man die Maximalgröße problemlos einstellen und mittels der Properties Width und Height kann man die Größe des Forms auslesen. Dabei passiert auch nicht das Problem, dass man schon bei 790 Pixeln hängen bleibt.

Der Grund für das Subclassing ist, dass die Out-of-the-Box-Events wie z.B. "Form_Resize" erst nach der Größenänderung des Formulars ausgelöst werden. Kontrolliert man dort dann das Seitenverhältnis o.ä., kommt es zu einem unschönen Flackern, da die Anpassung erst nach Anzeige der "falschen" bzw. ungewollten Größe geschieht (nämlich über das Form_Resize-Event).
Die mittels Subclassing abgefangenen Nachrichten wie WM_SIZING werden hingegen abgefangen, bevor optisch etwas zu sehen ist. So habe ich die Möglichkeit, vor der Anzeige etwas an den Fenstereigenschaften zu verändern. Lies Dir mal einige Sachen auf CodeProject und diversen VB-.NET-Seiten zur Kontrolle von Seitenverhältnissen u.ä. durch, da wird immer diese Methode verwendet. Aber ich lasse mich auch gern eines Besseren belehren.

Die Maximum/Minimum Size über die Form-Eigenschaften festzulegen, ist mir natürlich auch in den Sinn gekommen, allerdings beißt sich das mit dem Subclassing zum Erhalt des Seitenverhältnisses. Es kommt zum Springen der Spielfeld- und Previewkanten (PictureBoxes). Das funktioniert bei mir nur einwandfrei, wenn ich das Subclassing weglasse. Fragt mich aber nicht, warum das so ist. Aus diesem Grund habe ich die Min-/Max-Geschichte selbst implementiert.


Du brauchst dafür nicht eine einzige Win32-API manuell benutzen.

Das tue ich doch gar nicht?! :confused:

Der_Donnervogel
2008-08-06, 00:25:04
Der Grund für das Subclassing ist, dass die Out-of-the-Box-Events wie z.B. "Form_Resize" erst nach der Größenänderung des Formulars ausgelöst werden. Kontrolliert man dort dann das Seitenverhältnis o.ä., kommt es zu einem unschönen Flackern, da die Anpassung erst nach Anzeige der "falschen" bzw. ungewollten Größe geschieht (nämlich über das Form_Resize-Event).
Die mittels Subclassing abgefangenen Nachrichten wie WM_SIZING werden hingegen abgefangen, bevor optisch etwas zu sehen ist. So habe ich die Möglichkeit, vor der Anzeige etwas an den Fenstereigenschaften zu verändern.Da stellt sich bei mir die Frage wann und aus welchen Gründen wird denn neu gezeichnet? Mein Ansatz wäre einen Timer zu nehmen, und immer wenn der einen Event auslöst in einen BufferedGraphicsContext zeichnen und sobald alles gezeichnet ist diesen auf das Form zeichnen. Ein Resize könnte man (wenn der Timer ausreichend klein ist) also eigentlich was das Zeichnen betrifft ignorieren, bzw. einfach ein Neuzeichnen starten. Wenn das Neuzeichnen die aktuellen Größenverhältnisse berücksichtigt sehe ich keinen Grund warum da groß etwas flackern soll. Im folgenden mal ein ganz schneller Hack wie ich mir das ca. vorstellen würde:

Public Class Form1

Private Const columnCount As Integer = 10
Private context As BufferedGraphicsContext
Private grafx As BufferedGraphics
Private graphic As Graphics
Private blocks As New List(Of Block)
Private rand As New Random()
Private blockX As Integer = Width / columnCount
Private blockY As Integer = Height / 15

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
context = BufferedGraphicsManager.Current
context.MaximumBuffer = New Size(Me.Width + 1, Me.Height + 1)
grafx = context.Allocate(Me.CreateGraphics(), _
New Rectangle(0, 0, Me.Width, Me.Height))
DrawToBuffer(grafx.Graphics)

graphic = Me.CreateGraphics()

Dim column As Integer = rand.Next Mod columnCount
blocks.Add(New Block(New Rectangle(blockX * column, 0, blockX, blockY), column, Brushes.Brown))

Timer2.Interval = 100
Timer2.Start()
End Sub

Private Sub DrawToBuffer(ByVal g As Graphics)
g.Clear(Me.BackColor)
For Each b As Block In blocks
g.FillRectangle(b.color, b.rect)
Next
End Sub

Private Sub Form1_Resize(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Resize
blockX = Width / columnCount
blockY = Height / 15

context = BufferedGraphicsManager.Current
context.MaximumBuffer = New Size(Me.Width + 1, Me.Height + 1)
grafx = context.Allocate(Me.CreateGraphics(), _
New Rectangle(0, 0, Me.Width, Me.Height))

For Each b As Block In blocks
Dim newX As Integer = blockX * b.column
b.rect = New Rectangle(newX, b.rect.Y + 1, blockX, blockY)
Next
End Sub

Private Sub Timer2_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer2.Tick
For Each b As Block In blocks
b.rect = New Rectangle(b.rect.X, b.rect.Y + 1, b.rect.Width, b.rect.Height)
Next
DrawToBuffer(grafx.Graphics)
grafx.Render(graphic)
End Sub
End Class

Public Class Block
Public rect As Rectangle
Public color As Brush
Public column As Integer
Public Sub New(ByVal r As Rectangle, ByVal col As Integer, ByVal c As Brush)
rect = r
column = col
color = c
End Sub
End Class

Vielleicht kann der Code ja als Anregung dienen. Aber ehrlich gesagt programmiere ich nicht so oft in VB.Net (mehr Java und C#) und noch seltener muss ich selber was zeichnen. Es ist also schon möglich, dass die Variante die ich gewählt hätte Probleme macht.

dariegel
2008-08-06, 00:42:03
Da stellt sich bei mir die Frage wann und aus welchen Gründen wird denn neu gezeichnet? Mein Ansatz wäre einen Timer zu nehmen, und immer wenn der einen Event auslöst in einen BufferedGraphicsContext zeichnen und sobald alles gezeichnet ist diesen auf das Form zeichnen. Ein Resize könnte man (wenn der Timer ausreichend klein ist) also eigentlich was das Zeichnen betrifft ignorieren, bzw. einfach ein Neuzeichnen starten. Wenn das Neuzeichnen die aktuellen Größenverhältnisse berücksichtigt sehe ich keinen Grund warum da groß etwas flackern soll. Im folgenden mal ein ganz schneller Hack wie ich mir das ca. vorstellen würde:

Public Class Form1

Private Const columnCount As Integer = 10
Private context As BufferedGraphicsContext
Private grafx As BufferedGraphics
Private graphic As Graphics
Private blocks As New List(Of Block)
Private rand As New Random()
Private blockX As Integer = Width / columnCount
Private blockY As Integer = Height / 15

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
context = BufferedGraphicsManager.Current
context.MaximumBuffer = New Size(Me.Width + 1, Me.Height + 1)
grafx = context.Allocate(Me.CreateGraphics(), _
New Rectangle(0, 0, Me.Width, Me.Height))
DrawToBuffer(grafx.Graphics)

graphic = Me.CreateGraphics()

Dim column As Integer = rand.Next Mod columnCount
blocks.Add(New Block(New Rectangle(blockX * column, 0, blockX, blockY), column, Brushes.Brown))

Timer2.Interval = 100
Timer2.Start()
End Sub

Private Sub DrawToBuffer(ByVal g As Graphics)
g.Clear(Me.BackColor)
For Each b As Block In blocks
g.FillRectangle(b.color, b.rect)
Next
End Sub

Private Sub Form1_Resize(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Resize
blockX = Width / columnCount
blockY = Height / 15

context = BufferedGraphicsManager.Current
context.MaximumBuffer = New Size(Me.Width + 1, Me.Height + 1)
grafx = context.Allocate(Me.CreateGraphics(), _
New Rectangle(0, 0, Me.Width, Me.Height))

For Each b As Block In blocks
Dim newX As Integer = blockX * b.column
b.rect = New Rectangle(newX, b.rect.Y + 1, blockX, blockY)
Next
End Sub

Private Sub Timer2_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer2.Tick
For Each b As Block In blocks
b.rect = New Rectangle(b.rect.X, b.rect.Y + 1, b.rect.Width, b.rect.Height)
Next
DrawToBuffer(grafx.Graphics)
grafx.Render(graphic)
End Sub
End Class

Public Class Block
Public rect As Rectangle
Public color As Brush
Public column As Integer
Public Sub New(ByVal r As Rectangle, ByVal col As Integer, ByVal c As Brush)
rect = r
column = col
color = c
End Sub
End Class

Vielleicht kann der Code ja als Anregung dienen. Aber ehrlich gesagt programmiere ich nicht so oft in VB.Net (mehr Java und C#) und noch seltener muss ich selber was zeichnen. Es ist also schon möglich, dass die Variante die ich gewählt hätte Probleme macht.


Danke für Deine Mühe, dennoch glaube ich, dass Du mich falsch verstehst: nicht das gezeichnete Spielfeld flackert, sondern das Formular, bzw. der Rahmen und das Fenster selbst. Das Zeichen in eine PictureBox oder auf die Form ist nicht das Problem.

Das Problem ist, dass sich ein dynamisches Formular-Seitenverhältnis (abhängig von Spielfeld- und Vorschaugröße) aus irgendeinem Grund nicht mit einer maximalen Formulargröße vereinbaren lässt (beides per Subclassing). Wie gesagt, die Maximalgröße des Fensters wird nicht vernünftig übernommen (bzw. zu spät), sodass das Fenster bei Annäherung an die Maximalgröße immer zu früh "hängen" bleibt.

Oder steh' ich auf'm Schlauch und erkenne nicht, was Du mir sagen willst?!