Freigeben über


Dr. GUI .NET #7

 

31. Juli 2002

Inhalte

Einführung
Wo wir waren; Wohin wir gehen
Worum geht es im Leben?
Leben als Windows Forms Anwendung
Berechnen neuer Boards
Die Konsolen-Benutzeroberfläche
Benutzeroberfläche Windows Forms
Versuch es doch mal!
Was wir getan haben; Was kommt als nächstes

Teilen Sie uns und der Welt mit, was Sie über diesen Artikel auf dem Dr. GUI .NET Message Board auf dem Dr. GUI .NET Message Board denken. Es gibt einige gute Diskussionen, aber sie werden besser sein, wenn SIE mitmachen!

Sehen Sie sich die Beispiele an, die als ASP.NET Anwendungen mit Quellcode ausgeführt werden: http://coldrooster.com/DrGUIdotNet/. Das Leben ist noch nicht als Webanwendung da, aber das ist der nächste Schritt!

Einführung

Willkommen zurück zum neuesten Artikel von Dr. GUI über .NET Framework Programmierung – diesmal eine Anwendung von zweidimensionalen Arrays namens Conways Spiel des Lebens.

Wenn Sie nach den vorherigen Artikeln suchen, sehen Sie sich die Dr. GUI .NET-Startseite an.

Visual Basic, nur für diese Zeit

Dr. GUI hat beschlossen, dieses Mal Microsoft® Visual Basic® .NET zu verwenden, ohne den Code in C# zu verwenden.

Der gute Arzt fragt sich, was C#-, Java- und C/C++-Programmierer davon halten? Ist Visual Basic einfach genug, um zu lesen, dass Sie den Punkt erhalten können? Oder vermissen Sie wirklich C#-Code? Informieren Sie Dr. GUI in diesem Thread auf dem Dr. GUI .NET-Meldungsboard: https://www.gotdotnet.com/community/messageboard/Thread.aspx?id=29572.

Wo wir waren; Wohin wir gehen

Beim letzten Mal haben wir uns über Arrays im Microsoft®-.NET Framework unterhalten.

Dieses Mal zeigen wir ein Beispiel für die Verwendung von zweidimensionalen Arrays in einer Microsoft® Windows® Forms-Anwendung – Conways Game of Life.

Worum geht es im Leben?

Conways Spiel des Lebens wurde 1970 von John Conway, einem Mathematiker, erfunden. Es wurde in Martin Gardners Mathematische Spiele Kolumne in Scientific American in diesem Jahr beschrieben.

Die Grundregeln sind, dass Sie mit einem Muster von lebenden Zellen in einem Raster beginnen und dann jede Generation einen Satz Von Regeln anwenden, um zu bestimmen, welche Zellen sich für die nächste Generation im Raster befinden. Diese Regeln sind recht einfach:

Wenn ein leerer Fleck auf dem Raster genau drei lebende Nachbarn hat (von acht möglichen – Diagonalen zählen!), dann entsteht an diesem ehemals leeren Fleck ein neues Leben.

Eine lebende Zelle lebt weiter, wenn sie zwei oder drei Nachbarn hat. Wenn es weniger als zwei hat, stirbt es an Einsamkeit; wenn es mehr als drei hat, stirbt es an Überfüllung.

Aber aus diesen einfachen Regeln kann eine erstaunliche Vielfalt von Mustern und Automatisierungen gebildet werden.

Eine viel bessere, ausführlichere Beschreibung des Lebens und der Bedeutung des Lebens finden Sie im Artikel von Paul Callahan auf math.com und http://www.math.com/students/wonders/life/life.html Alan Hensels Seite Leben unter http://hensel.lifepatterns.net/.

Das Leben, das Universum und alles

Übrigens ist Life ein einfaches Beispiel für eine Klasse von Programmen, die als "Mobilfunkautomaten" bezeichnet werden. Physiker Stephen Wolfram ist in den Nachrichten für ein neues Buch, A New Kind of Science (http://www.amazon.com/exec/obidos/ASIN/1579550088/qid=1027637251/sr=8-1/ref=sr_8_1/104-0899262-1443926), das behauptet, dass zelluläre Automaten (nur etwas komplizierter als Conways Spiel des Lebens) die Funktionsweise des Universums modellieren können.

Leben als Windows Forms Anwendung

Unsere Version von Life ist sehr einfach: Das Board ist ziemlich klein, und die Benutzeroberfläche ist recht einfach. Der math.com Artikel und Alan Hensels Website haben Links zu anspruchsvolleren Versionen von Life, darunter einige, die viele Muster enthalten, in die Sie laden können, und einige, die riesige Universen ermöglichen. Unsere hat keine Möglichkeit, Muster zu speichern oder zu laden. (Ich denke, die Tatsache, dass wir Muster nicht speichern oder laden können, stellt sicher, dass wir Life erneut aufrufen werden, wenn es an der Zeit ist, über Dateien zu sprechen.)

So sieht die Benutzeroberfläche aus:

Die Schaltfläche Start startet den Generierungsprozess und wird zur Schaltfläche Beenden, sobald sie geklickt wird. Durch Klicken wird umgeschaltet, unabhängig davon, ob Generationen berechnet und automatisch angezeigt werden. Das Schiebereglersteuerelement steuert die Geschwindigkeit der Simulation. Die Schaltfläche "Einzelner Schritt" bewirkt, dass das Programm genau eine Generation erweitert.

Sie können das Board auch direkt bearbeiten, indem Sie darauf klicken. Oder Sie können es mit der Schaltfläche "Board löschen" löschen, einer vorhandenen Tafel mit der Schaltfläche "Zufällig hinzufügen" eine Reihe zufälliger Organismen hinzufügen oder die Tafel löschen und eine Reihe zufälliger Organismen mit der Schaltfläche "Löschen und Zufällig hinzufügen" hinzufügen.

Wenn das Programm Generationen ausführt, werden die Schaltflächen deaktiviert, die das Board löschen und dem Board zufällige Lebensdauer hinzufügen.

Berechnen neuer Boards

Bevor wir beschreiben, wie die Benutzeroberfläche funktioniert, sehen wir uns an, wie wir die nächste Generation tatsächlich berechnen.

Zunächst ist zu beachten, dass für die Berechnung der nächsten Generation ein zweites Array erforderlich ist, um die Ergebnisse zu speichern. Da alle Entscheidungen darüber, welche Zellen am Leben sind oder nicht, zur gleichen Zeit kommen, gibt es keine Möglichkeit, die Daten vorübergehend zu speichern, um den Rest zu berechnen. (Okay, naja, kein wirklich guter Weg.)

Ein einfältiger Algorithmus wäre für jede Zelle:

  • Besuchen Sie die benachbarten Zellen, um die Anzahl der Zellen für diesen Nachbarn zu berechnen.
  • Markieren Sie dann die entsprechende Zelle im neuen Array abhängig vom ursprünglichen Zustand der Zelle und der Anzahl der Nachbarn tot oder lebendig.

Das Problem mit diesem Algorithmus ist, dass Sie viel Zeit damit verschwenden, leere Zellen aufzufüllen (es sei denn, das Board ist sehr voll). Wenn sie jedoch sehr voll sind, wird es sich schnell leeren, da die meisten Zellen zu viele Nachbarn haben werden – so dass die Bretter nur selten annähernd voll sind, und nie seit mehr als einer Generation.

So ist dieser Algorithmus am Ende eine doppelt geschachtelte Schleife (O n^2), bei der wir für jede Zelle acht Speicherzugriffe und Ergänzungen durchführen müssen, auch wenn das Board in diesem Bereich leer ist.

Dies zu erkennen, inspiriert eine Optimierung: Anstatt Nachbarn für jede Zelle zu zählen, können wir jede lebende Zelle die Nachbaranzahl aller nachbarn inkrementieren. Wenn wir fertig sind, enthält jede Zelle im neuen Array die Anzahl der Nachbarn dieser Zelle. Mit diesen Informationen können wir durch einen schnellen Durchlauf der Arrays die Berechnung dieser Generation abschließen.

Wir haben immer noch eine doppelt geschachtelte Schleife, um alle Zellen zu durchlaufen, aber wir führen überhaupt keine Ergänzungen/Speicher durch, es sei denn, eine Zelle lebt. Die Tatsache, dass wir den Besuch von Nachbarn vermeiden, sollte mehr als ausgleichen, dass an jeder Zelle ein bedingter Test ausgeführt werden muss und die inkrementierten Werte wieder im Arbeitsspeicher gespeichert werden müssen – zumal viele Prozessoren über eine optimierte Inkrementanweisung verfügen.

Die Ecken und Kanten stellen einige Sonderfälle dar, da Ecken nur drei Nachbarn und Kanten nur fünf haben.

Ein einfacher Ansatz wäre das Hinzufügen von Code zum Test in der Standard-Schleife, um die speziellen Fälle zu behandeln. Sie könnten dies tun, aber alle Tests würden Ihr Programm erheblich verlangsamen. Stattdessen erledigen wir die Sonderfälle separat und verwenden Code, der genau das tut, was benötigt wird. Für jede der vier Ecken sieht der Code wie folgt aus:

    If inArray(0, 0) <> 0 Then
        outArray(0, 1) += 1
        outArray(1, 0) += 1
        outArray(1, 1) += 1
    End If

Es gibt eine dieser if-Anweisungen pro Ecke – insgesamt vier. Die anderen sind ähnlich. Klicken Sie hier, um es in einem neuen Fenster anzuzeigen.

Die nächsten Sonderfälle sind die Kanten (abzüglich der Ecken). Wir behandeln oben/unten und links/rechts in zwei Schleifen. Hier sehen Sie den Code, der den oberen und unteren Rand behandelt:

    ' then across top and bottom
    For j = 1 To width - 1
        If inArray(0, j) <> 0 Then
            outArray(0, j - 1) += 1
            outArray(1, j - 1) += 1
            outArray(1, j) += 1
            outArray(0, j + 1) += 1
            outArray(1, j + 1) += 1
        End If
        If inArray(height, j) <> 0 Then
            outArray(height, j - 1) += 1
            outArray(height - 1, j - 1) += 1
            outArray(height - 1, j) += 1
            outArray(height, j + 1) += 1
            outArray(height - 1, j + 1) += 1
        End If
    Next

Der Code zum Behandeln des linken und rechten Rands ist ähnlich. Klicken Sie hier, um es in einem neuen Fenster anzuzeigen.

Schließlich behandeln wir den Standard Fall – die mittleren Zellen, die alle acht Nachbarn haben:

   ' calculate number of neighbors in main part of inArray
   For i = 1 To height - 1
       For j = 1 To width - 1
           If inArray(i, j) <> 0 Then
               ' we have a life, so increment all neighbors' counts
               outArray(i - 1, j - 1) += 1
               outArray(i - 1, j) += 1
               outArray(i - 1, j + 1) += 1
               outArray(i, j - 1) += 1
               outArray(i, j + 1) += 1
               outArray(i + 1, j - 1) += 1
               outArray(i + 1, j) += 1
               outArray(i + 1, j + 1) += 1
           End If
       Next
   Next

Beachten Sie, dass wir keine Schleife verwendet haben, um die Nachbarn zu erhöhen. Dies hätte zusätzlichen Mehraufwand (zum Initialisieren und Verwalten des Schleifenzählers und zum Testen, wenn wir mit der Schleife fertig sind) und die Ausführung unseres Programms verlangsamt. Stattdessen haben wir einfach den Code geschrieben, um das zu tun, was direkt benötigt wird. Beachten Sie, dass wir uns auf den Compiler verlassen, um allgemeine Ausdrücke wie i + 1 aus dem obigen Code zu entfernen. Dies ist für moderne Compiler ziemlich einfach. Wenn Sie dies jedoch nicht tun, ist es einfach genug, temporäre Variablen zu erstellen, um sie zu speichern.

Sobald outArray die Anzahl der Nachbarn enthalten ist, können wir das neue Board mithilfe outArray's von Zählungen und dem ursprünglichen Muster in inArrayberechnen:

    ' use neighbor data and original data to determine life
    ' 0 or 1 in outArray
    For i = 0 To height
        For j = 0 To width
            If outArray(i, j) = 3 Then
                outArray(i, j) = 1
            ElseIf inArray(i, j) = 1 And outArray(i, j) = 2 Then
                outArray(i, j) = 1
            Else
                outArray(i, j) = 0
            End If
        Next
    Next

Wir haben zwei Überladungen von CalcNext bereitgestellt: eine, an die Sie ein Array übergeben, und es gibt ein neues (und neu zugeordnetes) Array zurück, und eine, die zwei vorhandene Arrays verwendet und daher keine neue Speicherzuordnung durchführt.

Die, die ein Array ordnet, ist besser für zustandslose Szenarien wie Microsoft® ASP.NET Programme oder Webdienste geeignet. Sie möchten die Arrays nicht beibehalten, da dies zu Problemen bei der Skalierung führt. Und der Aufwand beim erstellen des Arrays jedes Mal ist im Vergleich zu der Zeit für den Transport der Daten über das Kabel usw. klein – nicht Erwähnung im Vergleich zur Zeit zum Berechnen des neuen Arrays!

Die einfachere sieht wie folgt aus:

    ' call this overload if you need to create array each time
    ' anyway, as in ASP.NET.
    Public Function CalcNext(ByVal inOutArray(,) As Integer) As Integer(,)
        Return CalcNext(inOutArray, Nothing)
    End Function

... und ruft die andere auf (die ein neues Array ordnet, wenn Sie NULL/Nothing als zweiten Parameter übergeben):

    ' call this overload if you want to manage
    ' the input/output arrays yourself; returns output array
    Public Function CalcNext(ByVal inArray(,) As Integer, _
            ByVal outArray(,) As Integer) As Integer(,)

        ' In VB, lengths of dimensions are one more than max subscript,
        ' so we have to subtract one to get same size as original.
        ' We wouldn't have to in other languages.
        Dim width As Integer = inArray.GetLength(1) - 1 ' # cols
        Dim height As Integer = inArray.GetLength(0) - 1 ' # rows
        Dim i, j As Integer

        If (outArray Is Nothing) Then ' create array
            outArray = New Integer(height, width) {}
        Else
            CType(outArray, IList).Clear() ' clear existing array
        End If
      ' ...

Beachten Sie, dass unser Algorithmus für das Nachbarzählen erfordert, dass das Ausgabearray Nullen in allen Zellen enthält (da es die Arrayelemente erhöht, anstatt sie festzulegen). Wenn wir das Array neu erstellen, garantiert uns die .NET Framework dies. Wenn wir jedoch ein vorhandenes Array verwenden (und vermeiden, dass Speicherdruck durch wiederholtes Erstellen und Wegwerfen von Arrays entsteht), müssen wir es auf null reduzieren.

Hier weisen wir ein neues Array zu, wenn null/Nothing als zweiter Parameter übergeben wurde. Beachten Sie, dass Dr. GUI diese Methoden berücksichtigt (und tatsächlich geschrieben hat), sodass die Einzelparameterversion das Array zuordnen und übergeben würde. Die reale Version (zwei Parameter) musste jedoch wissen, ob das Array null war oder nicht (oder es würde Zeit verschwenden, ein neu zugeordnetes Array auf Null zu stellen, das bereits null war), sodass der gute Arzt die Zuordnung zur realen Methode verschoben und einen NULL/Nothing-Verweis verwendet, um anzugeben, ob ein neues Array zugeordnet werden musste oder nicht.

Die beste Möglichkeit zum Löschen eines Arrays ist die Verwendung der System.ArrayIList.Clear-Methode (bei der es sich um eine explizite Schnittstellenmethodenimplementierung der Clear-Methode aus IList handelt). System.Array verfügt ebenfalls über eine statische/freigegebene Clear-Methode , scheint aber nur für eindimensionale Arrays konzipiert zu sein. Es benötigt einen Anfangs- und Endpunkt des Elementbereichs, um auf 0 (null) festzulegen.

Wir verwenden die -Methode des Systems, anstatt eine eigene doppelt geschachtelte Schleife zu schreiben, da die Methode des Systems stark optimiert ist und so schnell wie möglich ausgeführt wird.

Unsere Methode hat also drei wichtige Optimierungen im Vergleich zum einfachsten möglichen Algorithmus:

  1. Es werden nur Ergänzungen für die Zellen ausgeführt, die live sind.
  2. Wir trennen unsere Sonderfälle, damit die Kernschleife sauber geschrieben werden kann und sie schnell ausgeführt wird.
  3. Wir heben die Registrierung der Schleifen für den Besuch aller Nachbarn auf, um den Schleifenmehraufwand zu vermeiden und damit der Compiler für uns optimieren kann.

Nicht speichereffizient, aber schnell

Sie werden feststellen, dass wir für jedes Leben 32-Bit-Ganzzahlen anstelle von 8-Bit-Bytes oder einzelnen Bits verwenden möchten. Dies ist eine riesige Speicherverschwendung, aber der gute Arzt tat dies aus einem Grund: Wenn wir etwas kleineres als ein Maschinenwort verwendet hätten, müsste der Prozessor oder der generierte Code etwas zusätzliches tun, um auf die einzelnen Bits für jedes Leben zuzugreifen. Bei einzelnen Bits würde dies viel Verschieben und Maskieren erfordern. Bei Bytes würde dies dazu führen, dass der Prozessor intern einige Verschiebungen und Maskierungen auf Byte-adressierbaren Systemen durchführt, oder auf Computern, die nicht byteadressierbar sind, müsste zusätzlicher Code generiert werden, um diese Verschiebung und Maskierung durchzuführen.

Unser Kompromiss bestand also in der Auswahl der Geschwindigkeit gegenüber der Arbeitsspeichergröße, indem wir die schnellste Datengröße auswählen.

Wenn wir planen, ein großes Board zu unterstützen, könnte es sich lohnen, ein Array von Bytes in Betracht zu ziehen, da der Zeittreffer auf byteadressierbaren Computern wahrscheinlich ziemlich klein ist und die Speichereinsparungen ein Faktor von vier sind. Die Verwendung einer kleineren Datenstruktur würde es dem Prozessor auch erleichtern, die Daten im Cache zu speichern. Und ein Byte ist groß genug, um die Anzahl der Nachbarn nachzuverfolgen (maximal acht!), sodass das kein Problem ist.

Es kann auch sinnvoll sein, einzelne Bits zu verwenden, wenn Sie eine Art Nachschlagetabellenschema verwenden können, um einen Teil der Berechnung durchzuführen, wie unten beschrieben.

Nicht sehr objektorientiert

Sie werden feststellen, dass dieser Algorithmus nicht der am wenigsten bitorientierte Objekt ist. Die objektorientierte Programmierung eignet sich sehr gut zum Erstellen von Abstraktionen, aber manchmal kommt der Mehraufwand wirklich im Weg. Dieses Mal ist eine dieser Zeiten. Stellen Sie sich vor, wie kompliziert (und langsam) das Berechnen einer neuen Generation wäre, wenn jede Zelle ein einzelnes Objekt wäre, für instance.

Eine weitere Beschleunigung

Dr. GUI hat dies noch nicht selbst getestet, aber er hat gesagt, im Gegensatz zu dem, was er in der letzten Spalte vermutet hat, dass Arrays von Arrays tatsächlich SCHNELLER als zweidimensionale Arrays sind, da der Code, der generiert wird, um sie zu verarbeiten, vom JIT-Compiler stärker optimiert wird.

Die Arrays von Arrays sind jedoch mühsam zuzuordnen (Sie müssen jede Zeile zusätzlich zum Standard Arrays separat zuordnen), sodass der gute Arzt sie diesmal nicht verwenden wollte. Der meiste Code würde fast gleich aussehen. Vielleicht gibt Dr. GUI es beim nächsten Mal einen Schuss, insbesondere wenn er einige Benchmarks zum Vergleichen der Leistung durchführt.

Eine andere Möglichkeit, es zu tun

Eine viel komplexere (und kompliziertere) Methode zur Berechnung von Lebensgenerationen finden Sie unter Alan Hensels Applet http://hensel.lifepatterns.net/lifeapplet.html. Anstatt ein zweidimensionales Array zum Speichern des Boards zu verwenden, verwendet er eine Liste von Bausteinen, die jeweils 16 x 16 sind. Wenn keine Organismen in einem bestimmten 16 x 16-Block vorhanden sind, wird der Block aus der Liste weggelassen, sodass der Hensel-Algorithmus nicht nur keine Zeit mit der Berechnung der Anzahl von Nachbarn für leere Bereiche verschwendet, er besucht diese Blöcke nicht einmal, da sie nicht in der Liste angezeigt werden.

Er optimiert weiter, indem er die 16 x 16 Blöcke in 8 x 8 Blöcke und schließlich 4 x 4 Blöcke unterteilt. Jeder 4 x 4-Block wird in einer 16-Bit-Ganzzahl gespeichert, und er verwendet eine Nachschlagetabelle, um die Ergebnisse für die mittlere 2 x 2-Region zu berechnen – eine enorme Zeitersparnis.

Sie können sehen, dass dieser Algorithmus sehr optimiert ist – sowohl durch die Berechnung leerer Bereiche als auch durch Die Verwendung einer Nachschlagetabelle anstelle einer Reihe von If- und Zuordnungsanweisungen. Zusätzlich zu dem Vorteil, sehr schnell zu arbeiten, hat es den Vorteil, dass viel weniger Arbeitsspeicher verwendet wird, als eine Matrix verwenden würde, da die Speicherauslastung proportional zur Anzahl der lebenden Zellen und nicht zur Gesamtgröße des Boards ist.

Eine Sache, die Sie bemerken, wenn Sie den Code überhaupt sorgfältig betrachten: Da Java nicht signierte Typen (und Arithmetik) nicht unterstützt, muss Alan eine angemessene Menge an Code (und Zeit) aufwenden, um Bits zu kompensieren. Visual Basic .NET unterstützt auch keine Typen ohne Vorzeichen, aber sowohl C# als auch die verwalteten Erweiterungen für C++ tun dies. Wenn Alan eines dieser Methoden verwenden würde, wäre sein Code etwas einfacher.

Die Konsolen-Benutzeroberfläche

Nach dem Schreiben der Berechnungs-Engine erstellte Dr. GUI zunächst eine sehr einfache Konsolenbenutzeroberfläche, um die Methoden der Lebensgenerierung zu testen. Klicken Sie hier, um den gesamten Benutzeroberflächencode der Konsole in einem neuen Fenster anzuzeigen.

Zuerst bat er den Benutzer, ihm die Anzahl der Zeilen und Spalten mitzuteilen, und erstellte das Array nach Spezifikation:

        Console.Write("Enter number of rows: ")
        Dim heightM1 As Integer = Console.ReadLine()
        Console.Write("Enter number of columns: ")
        Dim widthM1 As Integer = Console.ReadLine()
        heightM1 -= 1
        widthM1 -= 1
        Dim lifeBoard(heightM1, widthM1) As Integer ' subtract after in C#

Dann liest er Zeilen aus der Konsole, um Leben im Array zu erstellen:

        Dim i, j As Integer
        Dim s As String
        Dim response As Char

        Do
            For i = 0 To heightM1
                Console.Write("Enter row {0}: ", i)
                s = Console.ReadLine()
                s = s.PadRight(widthM1 + 1)
                For j = 0 To widthM1
                    If s.Chars(j) <> " " Then
                        lifeBoard(i, j) = 1
                    Else
                        lifeBoard(i, j) = 0
                    End If
                Next
            Next
            PrintLifeBoard(lifeBoard)

Schließlich ging er in eine Schleife, um jede nachfolgende Generation zu erledigen:

        Console.WriteLine("Next generation:")
        Do
            lifeBoard = LifeCalc.CalcNext(lifeBoard)
            PrintLifeBoard(lifeBoard)
            Console.Write("Enter to continue, ""n"" to play again," + _
                """x"" to stop: ")
            response = Console.ReadLine().ToLower()
        Loop While response <> "x" And response <> "n"
    Loop While response <> "x" ' matches with top Do...

Die PrintLifeBoard-Methode ist ganz einfach:

    Sub PrintLifeBoard(ByVal inArray(,) As Integer)
        Dim i, j As Integer
        Dim heightM1 As Integer = inArray.GetLength(0) - 1 ' # rows
        Dim widthM1 As Integer = inArray.GetLength(1) - 1 ' # cols
        Dim sb As New StringBuilder(New String(" ", widthM1 + 1))

        For i = 0 To heightM1
            For j = 0 To widthM1
                sb(j) = IIf(inArray(i, j) <> 0, "*", " ")
            Next
            Console.WriteLine(sb)
        Next
    End Sub

Beachten Sie, dass wir einen StringBuilder verwendet haben , um das Erstellen und Zerstören mehrerer Zeichenfolgen zu vermeiden (wie bei der Zeichenfolgenverkettung – ouch!), und wir haben die IIf-Funktion in Visual Basic verwendet, die dem Operator ? entspricht .

Die benutzeroberfläche Windows Forms

Eine Windows Forms Anwendung wird vollständig durch den Quellcode dargestellt, aber da Sie das Formular visuell in der Entwurfsansicht bearbeiten (und der Entwurfsansichts-Editor die Quelle ändert), lassen Sie uns kurz darüber sprechen, was sich im Formular befindet und welche Eigenschaften wir für diese Steuerelemente festgelegt haben.

Informationen zum Formular

Die Windows Forms Ui ist ziemlich einfach: Sie besteht aus einem Formular, das ein Bildfeld-Steuerelement, mehrere Schaltflächen, ein Textfeld und einen Schieberegler enthält.

Leser von Dr. GUI .NET rufen Schaltflächen und Textfelder aus vorherigen Spalten zurück. Das Bildfeld und der Schieberegler sind jedoch neu – ebenso wie die Verwendung der Gruppenfelder. Schließlich verwenden wir ein Zeitgebersteuerelement auf dem Formular, um periodische Ereignisse bereitzustellen, sodass wir eine neue Generation für jeden Timer -Tick berechnen und anzeigen können.

Eine Bildbox hat einen Wert von tausend Wörtern

Ein Bildfeld ist ein Steuerelement, das ein Bild enthält und es anzeigt. Der Vorteil der Verwendung besteht darin, dass Sie den Formular-Designer verwenden können, um es auf das Formular zu legen und das Formular anzulegen – und er verarbeitet die gesamte Zeichnung für Sie. Sie müssen lediglich festlegen, dass die Image-Eigenschaft auf ein Bild verweist, und pictureBox erledigt den Rest. (Würde Dr. GUI die Windows Forms Version des Zeichenprogramms umschreiben, die er vor ein paar Monaten gemacht hat, würde er diesmal eine PictureBox verwenden.)

Bildfelder sind auch nützlich, wenn die Größe des Formulars geändert wird, denn wenn Sie es richtig verankern, wird die Größe des Bildfelds damit geändert. Wenn Sie keine benutzerdefinierte Zeichnung durchführen, müssen Sie nichts tun. Wenn Sie dies tun, ist es einfach, es zum Funktionieren zu bringen. Dies wird später erläutert.

Schieberegler: Nicht nur von White Castle!

Diejenigen von Ihnen, die aus dem Mittleren Westen kommen, kennen vielleicht ein Restaurant namens White Castle. Sie verkaufen sehr kleine Hamburger, die unglaublich fettig sind – so fettig, dass sie oft als "Schieberegler" bezeichnet werden, weil sie klein genug und fettig genug sind, um ohne zu kauen. (Nicht wirklich, aber fast!)

Das Schieberegler-Steuerelement, über das wir sprechen, hat keine Beziehung zu White Castle-Schiebereglern.

Das Schiebereglersteuerelement stellt einen numerischen Wert dar und stellt eine Benutzeroberfläche in Form eines Schiebereglers bereit, damit der Benutzer den Wert ändern kann. Es ist einfach, die minimalen und maximalen Werte für den Wert festzulegen, den der Schieberegler darstellt, sowie die RightToLeft-Eigenschaft , was bedeutet, dass der Wert kleiner (nicht größer) wird, wenn Sie das Steuerelement nach rechts verschieben, wie es für dieses Problem erforderlich ist. Die Möglichkeit, diese Eigenschaften festzulegen, macht die Verwendung des Steuerelements sehr einfach, da Sie keinen Code schreiben müssen, um diese Aufgaben auszuführen.

Das Schiebereglersteuerelement löst Scrollereignisse aus, wenn der Schieberegler verschoben wird. Früher (vor Windows 95) gab es keine Schiebereglersteuerelemente, daher verwendeten Programmierer stattdessen Bildlaufleisten. Der Name des Ereignisses ist von den alten Tagen entfernt.

Beachten Sie, dass der Mindestwert für den Schieberegler ziemlich hoch festgelegt ist. Denn wenn die Zeit zum Berechnen einer neuen Generation und zum Zeichnen auf dem Bildschirm länger als das Zeitgeberintervall ist, erhält der Bildschirm nie die Möglichkeit, eine Aktualisierung durchzuführen. Dies ist ein größeres Problem, wenn der Bildschirm größer wird. Wir sehen uns an, dies später zu beheben. Eine Möglichkeit wäre, sicher zu sein, die Farbe zu erhalten und die Nachrichten jedes Mal durchzuklicken; eine andere wäre, die Generierungsberechnung und Bitmapzeichnung in einem Hintergrundthread durchzuführen. Beides ist für diesen Artikel zu kompliziert, daher stellen wir sicher, dass wir nicht versuchen, Generationen zu schnell zu erledigen.

Da wir nicht so schnell wie möglich ausgeführt werden (da wir eine Generation nur alle 100 ms ausführen), verwenden Sie bitte nicht die Windows Forms Anwendung, um Beurteilungen über die Geschwindigkeit dieses Programms zu treffen. Wenn Sie herausfinden möchten, wie schnell die Berechnungs-Engine ist, richten Sie einen Benchmark-Gurt ein.

Ein Gruppenfeld für Ihre Kinder

Die Bezeichnung, das Textfeld und die Schaltflächen in der oberen rechten Gruppe von Steuerelementen sind untergeordnete Elemente des Gruppenfelds, in dem sie enthalten sind. Da sie untergeordnete Elemente dieses Gruppenfelds sind, können wir sie alle gleichzeitig aktivieren und deaktivieren, indem wir das Gruppenfeld aktivieren oder deaktivieren (wodurch die untergeordneten Elemente automatisch aktiviert oder deaktiviert werden). Im Designer machen wir die anderen Steuerelemente zu untergeordneten Elementen des Gruppenfelds, indem wir das Steuerelement in das Gruppenfeld ziehen (oder es zuerst dort erstellen). Dies bewirkt, dass der Designer dem groupBox.Controls.AddRange() -Aufruf den Steuerelementnamen hinzugibt.

Ihr Timer tickt

Das Timer-Steuerelement ist interessant. Es ist auf dem Formular nicht sichtbar (der Designer zeigt es in einem kleinen Fenster direkt unter dem Formular an), aber auf alle anderen Arten verhält es sich wie ein sichtbares Steuerelement: Es verfügt über Eigenschaften, die Sie abrufen und festlegen können, und es löst Ereignisse aus. Für das Leben müssen wir ein regelmäßiges Ereignis erhalten, damit wir jedes Mal ein neues Board generieren können. Der Timer löst für uns die regelmäßigen Ereignisse aus, die als verstrichene Ereignisse bezeichnet werden. Unser Ereignishandler für das Elapsed-Ereignis berechnet eine neue Generation und zeigt sie an.

Während Dr. GUI das Life-Programm testet, bemerkte er eine seltsame Sache: Das Programm wurde ausgeführt. Daher hat er dem Load-Ereignishandler des Formulars eine -Anweisung hinzugefügt (dies wird später angezeigt), um den Timer zu beenden. Dadurch konnte das Spiel nicht mehr ausgeführt werden, sobald es begann.

Aber dann bemerkte der gute Arzt, dass die anfängliche Tafel NICHT zufällig war. Stattdessen sah es so aus, als ob das Spiel über mehrere Generationen gespielt worden wäre. Was war los? Der Timer wurde im Load-Ereignishandler für das Formular festgelegt!

Was passierte, war, dass der Timer als aktiviert initialisiert wurde, sodass er das Elapsed-Ereignis mindestens einmal ausgelöst hatte, bevor das Load-Ereignis aufgetreten war, und der Code im Handler schloss den Timer aus. Dieses Ereignis wurde in eine Warteschlange eingereiht und nach der Initialisierung des Boards in einem zufälligen Muster behandelt (nachdem der Load-Ereignishandler zurückgegeben wurde). Denken Sie daran, dass der verstrichene Handler eine Generation berechnet. Also wurde eine Generation berechnet, bevor die Anwendung vollständig initialisiert wurde und bevor der Bildschirm zuerst bemalt wurde!

Die richtige Lösung hierfür besteht darin, zum Designer zu wechseln und dessen Enabled-Eigenschaft auf False festzulegen. Dies ist, wie sich herausstellt, die Standardeinstellung, sodass dr. GUI die Eigenschaft beim Entwerfen des Formulars anscheinend irgendwann in True geändert hat. Nachdem der Timer beim Start deaktiviert wurde, funktionierte alles super!

Übrigens möchten Sie keinen benutzerdefinierten Code in die InitializeComponents-Methode einfügen, die innerhalb des ausgeblendeten Bereichs angezeigt wird. (Sehen, aber nicht anfassen!) In diesem Fall kann sie verloren gehen, wenn der Designer diese Methode bearbeitet.

Ändern der Größe des Formulars

Es gibt noch eine Sache, die wir tun müssen, um dies zu einer coolen Anwendung zu machen: Wir müssen in der Lage sein, die Größe des Formulars zu ändern und es weiterhin gut aussehen zu lassen.

Wie sich herausstellt, macht Windows Forms dies extrem einfach. Alles, was wir tun müssen, ist, die Anchor-Eigenschaft für die Steuerelemente richtig festzulegen, und die Größenänderung erfolgt automatisch. Wenn wir keine benutzerdefinierte Zeichnung ausführen, ist dies das Ende. (Wenn Sie es gewohnt sind, die Größe im Brand J-System zu ändern, werden Sie erstaunt sein, wie einfach dies ist.)

Das Hinzufügen zum Programm hat etwa zehn Minuten gedauert. Dr. GUI erinnert sich daran, was für ein Schmerz dies in MFC und Visual Basic und Windows in der Vergangenheit war, so dass er glaubt, dass es völlig erschüttert, dass dies in der .NET Framework so einfach ist.

Um zu verstehen, was hier vor sich geht, müssen Sie das Konzept der Verankerung verstehen.

Wenn Sie eine Seite eines Steuerelements an seinem Container verankern, bleibt diese Seite in einem festen Abstand von der entsprechenden Seite des Containers, unabhängig davon, wie die Größe des Containers geändert wird. Seiten von Steuerelementen, die nicht verankert sind, bleiben die gleiche Entfernung von der gegenüberliegenden Seite desselben Steuerelements – sie sind überhaupt nicht im Container verankert.

Das Schlüsselkonzept besteht darin, dass Steuerelemente standardmäßig oben und links von ihrem Container verankert sind. Das bedeutet, wenn die Größe des Containers (in diesem Fall das Formular) geändert wird, bleiben die oberen und linken Seiten der Steuerelemente relativ zur oberen linken Ecke des Containers in derselben Position.

Sie können auch nur unten und rechts verankern, in diesem Fall würden sich die Steuerelemente relativ zur unteren rechten Ecke des Containers verschieben. (Die obere und linke Seite des Steuerelements bleiben relativ zum unteren und rechten Rand des Steuerelements in derselben Position, sodass sich das gesamte Steuerelement verschieben würde.)

Interessant wird es, wenn Sie an gegenüberliegenden Seiten ankern, für instance, oben und unten, links und rechts oder alle vier Seiten. In diesen Fällen wird die Größe des Steuerelements mit der Containergröße geändert, sodass die entsprechende Seite denselben Abstand vom Rand des Containers behält, wie sich die Containergröße ändert.

Es ist sehr cool, dass Sie die Ergebnisse Ihrer Verankerung im Designer sehen können. Legen Sie einfach die Anker fest, und ändern Sie die Größe des Formulars im Designer.

Im Leben verankern wir die Bildbox an allen vier Seiten. Das bedeutet, dass die Größe des Fensters in beide Richtungen geändert wird – genau das, was wir möchten. Die Beschriftung oben auf dem Bild ist oben und links rechts verankert, sodass die Größe mit dem Bild geändert wird.

Wir verankern die anderen Steuerelemente rechts oben. Das bedeutet, dass sie in der oberen rechten Ecke des Formulars bleiben und dass sich ihre Größe nie ändert (da sie an keiner der gegenüberliegenden Seiten verankert sind). Oben rechts sind übrigens nur die Gruppenfelder und die Streutaste verankert. Da die anderen Steuerelemente untergeordnete Elemente der Gruppenfelder sind, werden sie im Gruppenfeld und nicht im Formular verankert.

Die Steuerelemente auf der rechten Seite haben also eine feste Größe und bleiben mit der oberen rechten Ecke – genau das, was wir möchten. Und das Bild ändert sich mit der Größe des Fensters – wieder genau so, wie wir möchten.

Wenn wir ein statisches Bild hatten, könnten wir die SizeMode-Eigenschaft des Bilds einfach auf StretchImage oder CenterImage festlegen. Wir können dies jedoch nicht mit unserem Bild tun, da dies die Bearbeitung sehr schwierig machen würde, da wir von ganzzahligen Größen für die Felder ausgehen und das Bild skaliert werden könnte, sodass unsere Klicks möglicherweise nicht wie erwartet aneinander angeordnet werden. Das bedeutet übrigens, dass das Bildfeld bis zu fast eine Zelle größer als das tatsächliche Bild sein kann.

Da wir eine benutzerdefinierte Zeichnung ausführen, müssen wir beim Ändern der Größe des Bildfelds eine Methode aufrufen, um die Parameter neu zu initialisieren, die die Zeichnung des Bilds steuern. Darüber werden wir später sprechen. Da wir die Parameter auf der Größe des Bildfelds basieren, ist es einfach, dieselbe Methode, SetImageAndCellSizes, aufzurufen, die wir bei der Initialisierung der Anwendung getan haben!

Beachten Sie, dass eine mögliche Möglichkeit zum Ändern der Größe der Anwendung darin besteht, sie zu minimieren – im Wesentlichen, die Größe der Anwendung auf Null zu ändern. Wie sich herausstellt, müssen wir das als Sonderfall behandeln.

Sehen wir uns also den Code für die Formularklasse an, die Membervariablen und Ereignishandler deklariert.

Code der Formularklasse

Dieses Formular ist insofern ungewöhnlich, als wir eine große Anzahl von Feldern haben. Aus Gründen der Effizienz behalten wir einige große Datenstrukturen bei – zwei Matrizen, eine Bitmap zum Zeichnen des Bilds und ein Graphics-Objekt zum Zeichnen in die Bitmap – sowie eine Reihe von Variablen, die wir nur einmal berechnen (es sei denn, wir ändern die Größe). Klicken Sie hier, um die gesamte Codedatei in einem neuen Fenster anzuzeigen.

Felder

Hier sehen Sie den Code mit allen unseren Felddeklarationen. Beachten Sie, dass einige Felder initialisiert werden, insbesondere werden die Arrays hier und jetzt erstellt.

Public Class LifeForm
    Inherits System.Windows.Forms.Form

' [Windows Form Designer generated code] (hidden region)

    ' test to make sure you have rows/cols right by making
    ' rows different from columns
    Const rows As Integer = 32
    Const cols As Integer = 32
    ' have to subtract one because VB allocates 0...max, 
    ' not 0...max - 1 as in C#
    Dim currBoard = New Integer(rows - 1, cols - 1) {}
    Dim tempBoard = New Integer(rows - 1, cols - 1) {}
    Dim img As Image
    Dim g As Graphics
    Dim imgWidth, imgHeight As Integer
    Dim widthCell, heightCell As Integer

Die Anzahl der Zeilen und Spalten wird als Konstanten erstellt, sodass sie in den folgenden Arraydeklarationen verwendet werden können. Wenn dies C# oder C++ wäre, müssten wir keines vom Tiefgestellt subtrahieren, da in diesen Sprachen ein 10-Element-Array seine Elemente von 0 bis 9 indiziert hat.

Visual Basic hat jedoch eine Legacy-Möglichkeit, anzugeben, dass die Untergrenze des Arrays 1 und nicht 0 ist. Diese Legacy wurde nicht auf Visual Basic .NET übertragen. Um das Portieren von Code mit der alten OPTION BASE 1-Anweisung zu vereinfachen, weist Visual Basic .NET jedoch ein zusätzliches Arrayelement zu. Wenn Sie also 10 Elemente angeben, erhalten Sie tatsächlich 11, nummeriert 0 bis 10.

Wenn Sie tatsächlich nur 10 Elemente möchten, subtrahieren Sie einfach eines vom Tiefgestellt, wenn Sie das Array in Visual Basic erstellen, wie hier.

Als Nächstes weisen wir Verweise für das Image-Objekt zu, das wir zum Zeichnen unseres Lebensboards verwenden, und ein Graphics-Objekt , das wir verwenden, um eine Zeichnungsschnittstelle für das Bild bereitzustellen. Schließlich werden ganze Zahlen zugeordnet, um die Höhe und Breite des gesamten Bilds sowie die Breite und Höhe einer einzelnen Zelle im Bild zu halten.

Die Werte für diese Variablen hängen von der Größe des Clientbereichs des Bildfelds (sowie von der Größe des Arrays im Fall der Zellengrößen) ab, sodass wir diese Variablen erst initialisieren können, wenn das Bildfeld erstellt und initialisiert wird. Der Load-Ereignishandler für das Formular ist der ideale Zeitpunkt für solche Initialisierungen, daher werden wir dort unseren durchführen.

Initialisieren und Bereinigen

Geladen werden

Wenn das Formular geladen wird, müssen wir die Initialisierung um das Bildfeld und das Bild herum durchführen. Wir können dies nicht früher tun, da wir nicht wissen, wie groß die Bildbox ist.

Der Code lautet wie folgt:

    Private Sub LifeForm_Load(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles MyBase.Load
        FillRandom(200, False)
        SetImageAndCellSizes()
    End Sub

Wie Sie sehen, füllen wir einfach das Array mit zufälligem Leben und richten dann unser Bild richtig ein. So funktioniert das Einrichten der Images ( FillRandom wird später angezeigt):

    Private Sub SetImageAndCellSizes()
        ' set image and cell sizes
        imgWidth = BoardImage.ClientSize.Width
        imgHeight = BoardImage.ClientSize.Height
        widthCell = imgWidth \ (cols) ' integer division
        heightCell = imgHeight \ (rows)
        ' adjust virtual image size to exact maximum (might shrink),
        ' prevent(exceptions) on clicks on extreme right/bottom
        imgWidth = widthCell * cols
        imgHeight = heightCell * rows
        If Not (img Is Nothing) Then ' if we have one, we have both
            img.Dispose()
            g.Dispose()
        End If
        img = New Bitmap(imgWidth, imgHeight)
        g = Graphics.FromImage(img)
        BoardImage.Image = DrawBoard()
    End Sub

Wir erhalten die Größe des Clientbereichs (Zeichnungsbereich) des Bildfelds. Dann ermitteln wir, wie groß jede Zelle in Pixeln ist, und runden sie auf eine ganze Zahl ab. Wir erhalten die Größe der Bitmap, indem wir die Arraygröße mit der Größe jeder Zelle multiplizieren. Dies ist dasselbe oder ein wenig kleiner als (bis zu einer Zellengröße minus 1) das Bildfeld, aber das ist in Ordnung – es bleibt die Mathematik einfach.

Nachdem wir die Bitmap erstellt haben, weisen wir ein Grafikobjekt zu, das dazu verwendet werden soll. Die Zeichnung wird mithilfe des Grafikobjekts ausgeführt, aber die Zeichnung wird in der zugeordneten Bitmap angezeigt.

In beiden Fällen wird "Dispose " für die alte Bitmap und die alten Grafiken aufgerufen, sofern eine alte Bitmap vorhanden ist. Wenn Ihr Objekt über eine Dispose-Methode verfügt, ist es wichtig, dass Sie es aufrufen, bevor Sie den letzten Verweis darauf zerstören. Der Garbage Collector wird schließlich nach Ihnen sauber, ist aber möglicherweise nicht schnell genug, um knappe Ressourcen ohne Arbeitsspeicher aufzuholen. Und wenn am Ende viele Bitmaps und Grafikobjekte hängen bleiben, kann die Leistung Ihres Systems beeinträchtigt werden. Sauber also nach sich selbst!

Zeichnung

Die DrawBoard-Methode übernimmt die eigentliche Zeichnung des Boards. Es gibt einen Verweis auf das Bild zurück, auf dem es gezeichnet wurde (die Bitmap). Wir verwenden diesen zurückgegebenen Verweis, um die Image-Eigenschaft unseres Bildfelds festzulegen. Wenn dies festgelegt ist, wird der Bildschirm aktualisiert.

Hier ist DrawBoard:

    Private Function DrawBoard() As Image
        ' black background
        g.FillRectangle(Brushes.Black, 0, 0, imgWidth, imgHeight)

        Dim i, j As Integer
        For i = 0 To rows - 1
            For j = 0 To cols - 1
                If currBoard(i, j) <> 0 Then
                    g.FillEllipse(Brushes.HotPink, _
                        j * widthCell, i * heightCell, _
                        widthCell, heightCell)
                End If
            Next
        Next
        Return img
    End Function

Alles, was es tut, ist, den Hintergrund in Schwarz zu löschen und dann eine Auslassungspunkte für jeden Ort zu zeichnen, an dem eine Zelle lebt – sehr einfach.

Eine andere Strategie für das Grafikobjekt – und vielleicht sogar für die Bitmap – wäre, es in der DrawBoard-Methode zu erstellen und zu zerstören. Da wir nicht sehr viele von ihnen verwenden (nur eine, wenn wir die Größe nicht ändern), scheint es sinnvoller, sie einmal zu erstellen und sie so lange beizubehalten, bis wir mit ihnen fertig sind.

Wenn wir jedoch eine zustandslose Anwendung wären – eine Webanwendung oder ein Webdienst –, dann würden wir sofort nach uns selbst sauber. Daher erstellen wir die Bitmap und das Grafikobjekt in DrawBoard, und achten Sie dann darauf, dispose für jeden aufzurufen, bevor die Methode endet.

Entladen

Da wir die Bitmap und Grafiken beibehalten, sollten wir die letzten Grafiken und Bitmaps sauber, wenn die Anwendung geschlossen wird.

    Private Sub LifeForm_Closed(ByVal sender As Object, _
            ByVal e As System.EventArgs) Handles MyBase.Closed
        g.Dispose()
        img.Dispose()
    End Sub

Behandeln der Ereignisse

Der Rest des Codes sind größtenteils Ereignishandler. Es gibt einige Methoden, die wir berücksichtigen könnten, aber die meisten behandeln nur Ereignisse.

Bearbeiten des Boards

Wir bearbeiten das Board, indem wir die vom Bildfeld generierten Mousedown-Ereignisse abrufen (ein weiterer Grund, ein Bildfeld zu verwenden!) und die entsprechende Position in der Matrix festzulegen. Beachten Sie, dass das Bildfeld möglicherweise größer als das Bild ist. Daher müssen Wir nach Klicks außerhalb der Bildgröße suchen und sie ignorieren.

    Private Sub BoardImage_MouseDown(ByVal sender As System.Object, _
            ByVal e As System.Windows.Forms.MouseEventArgs) _
            Handles BoardImage.MouseDown
        If e.Y < imgHeight And e.X < imgWidth Then ' skip if outside image
            If IntervalTimer.Enabled Then
                StopTimer()
            End If
            ' integer division below!
            Dim row As Integer = e.Y \ heightCell
            Dim col As Integer = e.X \ widthCell
            currBoard(row, col) = currBoard(row, col) Xor 1
            BoardImage.Image = DrawBoard()
        End If
    End Sub

Wir haben das Mouse-Down-Ereignis anstelle eines anderen Ereignisses aus zwei Gründen behandelt: Das offensichtliche Ereignis, das behandelt werden soll , wäre Click. Das Click-Ereignis bietet Ihnen jedoch keine Möglichkeit, herauszufinden, WO der Klick aufgetreten ist. Der an Ihren Handler übergebene EventArgs-Parameter enthält überhaupt keine Daten. Wir mussten also entweder die Maus nach oben oder unten behandeln.

Dr. GUI bevorzugt dafür die Maus nach oben, aber es gibt ein subtiles Problem, das ihn gezwungen hat, mausabwärts zu verwenden. Hier ist das Problem: Wenn Sie das Fenster maximieren, indem Sie auf die Titelleiste doppelklicken, und wenn sich die Stelle, auf die Sie doppelklicken, im Bild des größeren Bildfelds befindet, wird das Mouse-up-Ereignis an das Bildfeld gesendet, wodurch das Board bearbeitet wird. Der gute Arzt ist der Meinung, dass Windows diese Mouse-up-Nachricht essen sollte, wenn sie maximiert, aber das scheint es nicht zu tun. Allerdings funktioniert die Handhabung der Maus stattdessen ziemlich gut, sodass es nicht das Ende der Welt ist.

Berechnen einer neuen Generation

Die SingleStepButton_Click-Methode bewirkt, dass eine neue Generation berechnet und angezeigt wird. Zuerst wird der Timer beendet, wenn er ausgeführt wird. Wenn der Timer nicht ausgeführt wurde, wird NextGeneration aufgerufen.

    Private Sub SingleStepButton_Click(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) _
            Handles SingleStepButton.Click
        If IntervalTimer.Enabled Then
            StopTimer() ' and otherwise ignore click
        Else
            NextGeneration()
        End If
    End Sub

NextGeneration ruft CalcNext auf (zuvor gesehen!), wobei beide Boards übergeben werden. Das neue Board wird durch Verweis von der -Methode zurückgegeben, sodass wir sie speichern und dann den Rest des Codes schreiben, um die beiden Boards zu tauschen. Das Austauschen der Boards bedeutet, dass wir nie ein neues Board erstellen müssen. Schließlich zeichnen wir den Inhalt der Tafel in eine Bitmap und legen die Image-Eigenschaft des Bildfelds darauf fest, damit es auf dem Bildschirm gezeichnet wird.

    Private Sub NextGeneration()
        ' swap boards so we don't have to create a new one each time
        Dim newBoard As Integer(,) = _
            LifeCalc.CalcNext(currBoard, tempBoard)
        tempBoard = currBoard
        currBoard = newBoard
        ' skip drawing, save CPU if minimized
        If Not BoardImage.Size.IsEmpty Then
            BoardImage.Image = DrawBoard()
        End If    End Sub

Beachten Sie, dass wir das Zeichnen des Boards überspringen, wenn unser Fenster minimiert ist. Wenn der Timer aktiviert ist, berechnen wir weiterhin automatisch bei jedem Zeitgeberstrich eine neue Generation, schreiben sie jedoch nicht in eine Bitmap, wenn sie nicht angezeigt wird.

Die oberste Groupbox

Die Schaltflächen im oberen Gruppenfeld löschen das Board und/oder fügen ein zufälliges Muster darin ein. Sie sind ganz einfach:

    Private Sub ClearButton_Click(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles ClearButton.Click
        CType(currBoard, IList).Clear()
        BoardImage.Image = DrawBoard()
    End Sub

    Private Sub ClearRandomButton_Click(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles ClearRandomButton.Click
        FillRandom(NumRandom.Text, True)
        BoardImage.Image = DrawBoard()
    End Sub

    Private Sub AddRandomButton_Click(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles AddRandomButton.Click
        FillRandom(NumRandom.Text, False)
        BoardImage.Image = DrawBoard()
    End Sub

Die FillRandom-Methode ist ebenfalls recht einfach. Beachten Sie, dass Sie nicht garantiert die Anzahl der Zellen erhalten, die Sie anfordern. Es versucht nur, diese Anzahl von Zellen festzulegen. Es ist möglich (in der Tat wahrscheinlich), dass einige der Zellen mehr als einmal festgelegt werden. Da wir nach leeren Zellen suchen müssten, um dies zu beheben, entschied sich der gute Arzt, es einfach zu halten:

    Private Sub FillRandom(ByVal maxCells As Integer, _
            ByVal clear As Boolean)
        Dim i, j As Integer
        Dim r As New Random()
        Dim height As Integer = currBoard.GetLength(0) ' # rows
        Dim width As Integer = currBoard.GetLength(1) ' # cols
        If (clear) Then
            CType(currBoard, IList).Clear()
        End If
        For i = 1 To maxCells ' can fill spots > once, that's OK
            currBoard(r.Next(height), r.Next(width)) = 1
        Next
    End Sub

Größenänderung

Das Ändern der Größe ist im Code eigentlich sehr einfach. Die .NET Framework erledigt fast die gesamte Arbeit für uns.

Eine Sache, die für uns nicht möglich ist, ist, die Daten zurückzusetzen, die wir zum Generieren des bildgerechten Bilds verwenden. Dies ist jedoch ganz einfach, indem wir SetImageAndCellSizes aufrufen:

    Private Sub BoardImage_Layout(ByVal sender As Object, _
            ByVal e As System.Windows.Forms.LayoutEventArgs) _
            Handles BoardImage.Layout
        ' just skip if image minimized
        If Not BoardImage.Size.IsEmpty Then
            SetImageAndCellSizes()
        End If    End Sub

Beachten Sie, dass wir das Festlegen der Bildgrößen überspringen, wenn das Formular minimiert wird. (Wenn das Formular minimiert wird, wird die Größe des Bildfelds auf 0 x 0 geändert. Wenn wir den Funktionsaufruf nicht überspringen, erhalten wir eine Ausnahme vom Versuch, eine Bitmap im Format 0 x 0 zu erstellen.

Last but not least: der Timer

Wir haben einige Methoden, die sich mit dem Timer befassen. Der einfachste ist der Elapsed-Ereignishandler . Sie ruft nur NextGeneration auf, um die nächste Generation zu berechnen und anzuzeigen:

    Private Sub IntervalTimer_Elapsed(ByVal sender As System.Object, _
            ByVal e As System.Timers.ElapsedEventArgs) _
            Handles IntervalTimer.Elapsed
        NextGeneration()
    End Sub

Wir aktivieren und deaktivieren den Timer mit diesem Satz von drei Methoden. TimerToggleButton_Click ruft nur die richtige Methode auf, abhängig vom aktuellen Zustand der Zeit (aktiviert oder nicht):

    Private Sub TimerToggleButton_Click(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles TimerToggleButton.Click
        If Not IntervalTimer.Enabled Then
            StartTimer()
        Else
            StopTimer()
        End If
    End Sub

StartTimer und StopTimer aktivieren und deaktivieren die Benutzeroberfläche und ändern die Schaltflächenbeschriftung zusätzlich zum Starten und Beenden des Timers:

    Private Sub StartTimer()
        AddGroupBox.Enabled = False
        TimerToggleButton.Text = "Stop"
        IntervalTimer.Interval = SpeedSlider.Value
        IntervalTimer.Start()
    End Sub

    Private Sub StopTimer()
        IntervalTimer.Stop()
        TimerToggleButton.Text = "Start"
        AddGroupBox.Enabled = True
    End Sub

Und der Handler für das Scroll-Ereignis des Schiebereglers kann nicht einfacher sein. Da der Wert des Schiebereglers bereits ordnungsgemäß skaliert wurde (weil wir die Eigenschaften richtig festlegen), müssen wir diesen Wert lediglich in den Timer kopieren:

    Private Sub SpeedSlider_Scroll(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles SpeedSlider.Scroll
        IntervalTimer.Interval = SpeedSlider.Value
    End Sub

Und, ob Sie es glauben oder nicht, das ist es!

Vielen Dank!

Dr. GUI muss einigen Leuten danken, die dabei geholfen haben.

Zunächst haben mehrere Personen den Code überprüft: Matt Powell, Priya Dhawan und Curtis Man. Jedes bot Kommentare an, die es besser gemacht haben. Curtis inspirierte in einer unterhaltsamen und langen Unterhaltung auch den effizienteren CalcNext-Algorithmus , den wir verwendet haben. Vielen Dank, alle!

Henry Borys bearbeitet dr. GUI, was keine einfache Aufgabe ist, aber Dr. GUI ist viel besser als ergebnis. Vielen Dank, Henry!

Schließlich muss Dr. GUI seinem Manager Pedro Silva danken, der, obwohl der Artikel zu spät und am Stichtag ausgeführt wurde, dem guten Arzt darüber erzählte, wie cool größenänderung in der .NET Framework ist. Es war so cool, dass Dr. GUI nicht widerstehen konnte – und es dauerte nur etwa eine Stunde, einschließlich des Schreibens darüber. sehr gut! Vielen Dank!

Versuch es doch mal!

Wenn Sie .NET haben, können Sie es ausprobieren. Wenn Sie es nicht haben, sollten Sie es in Betracht ziehen. Wenn Sie eine Stunde oder so eine Woche mit Dr. GUI .NET verbringen, sind Sie in kürzester Zeit ein Experte im .NET Framework!

Seien Sie der Erste in Ihrem Block – und laden Sie einige Freunde ein!

Es ist immer gut, der Erste zu sein, der eine neue Technologie lernt, aber noch mehr Spaß, dies mit einigen Freunden zu tun! Organisieren Sie für mehr Spaß eine Gruppe von Freunden, um gemeinsam .NET zu lernen!

Einige Dinge, die Sie ausprobieren können...

Testen Sie zunächst den hier gezeigten Code. Ein Teil davon stammt aus größeren Programmen; Sie müssen das Programm um diese Codeausschnitte herum aufbauen, was eine bewährte Methode ist. (Oder verwenden Sie den Code, den der gute Arzt bereitstellt, wenn Sie müssen.) Versuchen Sie, mit dem Code einige zu spielen. Hier sind einige Ideen:

  • Spielen Sie mit verschiedenen Mustern, und genießen Sie.
  • Spielen Sie mit verschiedenen Regeln, und sehen Sie, wie sich dies ändert.
  • Ändern Sie das Programm so, dass es die Größe des Arrays anstelle der Größe der Zellen ändert, wenn die Größe des Bilds geändert wird. Fügen Sie der Benutzeroberfläche ein Kontrollkästchen hinzu, damit der Benutzer entscheiden kann, ob die Größe des Arrays geändert werden soll (und dadurch ein neues Array erstellt wird), wenn die Größe des Fensters geändert wird.
  • Schreiben Sie einen Benchmarking-Harness, um ein großes Life-Board zu erstellen. Fügen Sie einige Zellen ein (Hinweis: Seeden Sie den Zufallszahlengenerator mit einem konsistenten Seed, und das Muster wird jedes Mal gleich sein!) und die Zeit, in der einige hundert Generationen ausgeführt werden.
  • Testen Sie mithilfe des Benchmarking-Harnesss ein Array von Arrays anstelle eines zweidimensionalen Arrays, und sehen Sie, welches Array schneller ist.
  • Versuchen Sie mithilfe des Benchmarking-Harnesss, den Berechnungscode in C#, C# mit Zeigern (unsicher!), verwaltetem C++ und nativem C++-Code (Zugriff über COM-Interop) zu schreiben. Was ist am schnellsten? Am einfachsten zu schreiben? Am schwersten zu schreiben?
  • Ändern Sie die Benutzeroberfläche auf interessante Weise. Probieren Sie eine Vielzahl von Windows Forms-Steuerelementen aus, und sehen Sie sich an, was sie tun.
  • Überprüfen Sie, ob Sie das Timer-/Zeichnungsproblem lösen können.

Was wir getan haben; Was kommt als nächstes

Dieses Mal haben wir Conways Game of Life als Windows Forms Anwendung gezeigt. Das nächste Mal führen wir dies als ASP.NET-Anwendung aus.