Lektion 22 - Oberflächentransport 1

Erstellt von Frithjof Sun, 21 Sep 2008 21:44:00 GMT

Die Hello World Oberfläche der letzten Lektion zeigte dir den allgemeinen Umgang mit FXRuby. Damit kannst du aber auf die Dauer niemanden beeindrucken. Zu einer richtigen Oberfläche gehört mehr. Im Vergleich zur Programmierung an der Konsole, wo jede Ausgabe auf einer Zeile stattfindet und der Rest einfach eine Zeile nach oben rutscht, steht dir hier ein bestimmter Bereich an der Oberfläche für Ein- und Ausgaben zur Verfügung, der eine bestimmte Breite und Höhe hat. Ein Widget irgendwo auf dieser Oberfläche hat selbst wieder eine bestimmte Breite und Höhe.

Diese Lektion will dir ein besseres Gefühl für diese zweidimensionale Anordenbarkeit geben.

Du baust eine Oberfläche, die auf der linken Seite mit einer großen Leinwand und auf der rechten Seite mit einer Leiste für Drucktasten ausgestattet ist und noch ein wenig was drumherum.

Allgemeiner Aufbau

Die beiden wichtigen FXRuby Objekte für die Application und das MainWindow legen wir diesmal auch gleich etwas anders an, als in der letzten Lektion.


# Copyright (C) 2007-2008 www.rubykids.de Frithjof Eckhardt
# Alle Rechte vorbehalten.
# lektion_22.rb

require 'fox16'
require 'fox16/colors'

include Fox

class RubykidsMainWindow < FXMainWindow
  def initialize(app)
    super(app, "Rubykids.de", :opts => DECOR_ALL, :width => 800, :height => 600)

  end

  def create
    super
    show(PLACEMENT_SCREEN)

  end

end

application = FXApp.new
    mainWin = RubykidsMainWindow.new(application)
application.create
application.run

Für die Verwendung von Farben stellt FXRuby eine paar Klassen und vordefinierte Variablen bereit, die wir unserem Programm mit dem require ‘fox16/colors’ bekannt machen.

Mit include Fox machen wir das Modul Fox unserem Programm bekannt. Über Module hatten wir noch nicht genauer gesprochen, stell dir einfach eine Klasse darunter vor. Dieses include Fox gestattet uns nun überall den Prefix Fox:: in unserem Programm wegzulassen. Statt Fox::PLACEMENT_SCREEN brauchen wir nun nur noch PLACEMENT_SCREEN zu schreiben.

Als nächstes definieren wir unser eigenes MainWindow als Klasse RubykidsMainWindow. Diese Klasse erbt von der Klasse FXMainWindow. Damit sie sich auch wie ein ordentliches MainWindow verhält, muss sie im Konstruktor initialize ihre Oberklasse mit dem Befehl super aufrufen. Dabei geben wir in dem Parameter opts auch neuerdings gleich die Größe unseres Hauptfensters mit 800 Pixeln Breite und 600 Pixeln Höhe an.

Eine weitere Methode hat unsere Hauptfenster-Klasse. Die Methode create dient dazu, am Anfang das Fenster aufzubauen und alles, was der User von Beginn an sehen soll darauf zu platzieren. Diese create Methode des Hauptfensters wird später von der gleichnamigen create Methode des Application Objekts application.create aufgerufen.

Das probierst du gleich einmal aus.


[21.09.2008, 21:16]:> ruby lektion_22.rb

Es funktioniert zwar, aber es fehlt noch eine Menge.

Die Layoutmanager

Auch wenn eine Oberfläche im Kopf schon fertig ist, lege trotzdem immer noch eine Skizze dafür an. Ich stelle mir unsere Oberfläche, die wir in dieser Lektion schaffen wollen etwas so vor:

Bei einer Oberfläche gibt es neben den sichtbaren Widgets auch unsichtbare Dinger, die bestimmte Aufgaben erfüllen. Dazu gehören die Layoutmanager. Sie sieht man nicht, sie sorgen aber dafür, dass alle von ihnen betreuten Widgets an der Oberfläche richtig angeordnet werden. Ein Layoutmanager ist somit eine Art Container, in den man die Widgets hineinlegt, um die er sich kümmern soll.

Wir verwenden zwei der Layoutmanager von FXRuby. Der FXHorizontalFrame ordnet alle seine Widgets horizontal, also von links nach rechts an und zwar in der Reihenfolge, wie sie ihm übergeben werden. Der FXVerticalFrame Layoutmanager macht dasselbe, nur eben vertikal, also von oben nach unten.

An der Skizze kannst du vielleicht schon ablesen, wieviele solcher Layoutmanager wir brauchen:

Einen FXHorizontalFrame, der sich um den gesamten Bereich kümmert. Nennen wir ihn hauptFrame.

Dem hauptFrame übergeben wir zwei weitere Layoutmanager: Einen FXVerticalFrame, der sich um die Überschrift Malbereich und den weißen Malbereich, die Leinwand, kümmert. Nennen wir ihn leinwandFrame. Und einen FXVerticalFrame für die Überschrift Menü und die eine Drucktaste zum Beenden, nennen wir ihn menuFrame.

Alle drei Layoutmanager erzeugen wir in der initialize Methode unseres eigenen Hauptfensters:


...
def initialize(app)
  super(app, "Rubykids.de", :opts => DECOR_ALL, :width => 800, :height => 600)

  @hauptFrame = FXHorizontalFrame.new(
    self,
    LAYOUT_SIDE_TOP|LAYOUT_FILL_X|LAYOUT_FILL_Y,
    :padLeft   => 0,
    :padRight  => 0,
    :padTop    => 0,
    :padBottom => 0
  )

  # * * * Linker Leinwandbereich
  @leinwandFrame = FXVerticalFrame.new(
    @hauptFrame,
    LAYOUT_TOP|LAYOUT_LEFT|LAYOUT_FILL_X|LAYOUT_FILL_Y,
    :padLeft   => 20,
    :padRight  => 20,
    :padTop    => 20,
    :padBottom => 20
  )

  # * * * Rechter Bereich für Buttons
  @menuFrame = FXVerticalFrame.new(
    @hauptFrame,
    LAYOUT_TOP|LAYOUT_LEFT|LAYOUT_FILL_Y,
    :padLeft   => 20,
    :padRight  => 20,
    :padTop    => 20,
    :padBottom => 20
  )
end
...

Schauen wir uns den hauptFrame etwas genauer an. Als ersten Parameter erhält er self, also unser eigenes Hauptfenster der Klasse RubykidsMainWindow. Der nächste Parameter ist ein zusammengesetzter Parameter, der etwas ungewöhnlich aussieht. Er besteht aus drei Unterparametern, die mit einem | Strich getrennt sind.

Diese Schreibweise ist in den Sprachen C und C++ üblich, um in einem Parameter gleich mehrere verschiedene Optionen festlegen zu können. Du erinnerst dich an meine einleitenden Worte zu FXRuby? Hier siehst du ein Beispiel für so eine ekelige Sache, die man von dem weiter unten liegenden C++ Programmcode übernommen hat. Das genau zu erklären, wäre Stoff für eine eigene Lektion. Willst du es genau wissen, dann google nach c++ bit operation.

Wir merken uns nur die Bedeutung der Unterparameter. LAYOUT_SIDE_TOP bedeutet, der Layoutmanager fängt mit seiner Arbeit ganz oben (links) an, erstreckt sich dann ganz nach rechts, LAYOUT_FILL_X (das ist die X-Richtung, Breite des Fensters) und ganz nach unten, LAYOUT_FILL_Y (das ist die Y-Richtung, Höhe des Fensters).

Die anderen vier Parameter, die mit pad im Namen Beginnen, legen den Abstand zum umgebenden Rand fest. Also padLeft gibt an, wie groß der Abstand am linken Rand zum umgebenden Bereich sein soll. Achte auch hier auf die Schreibweise bei der Übergabe der Parameter. Es handelt sich bei diesen vier pad-Parametern jeweils um einen Hash mit einem Element. Das ist ein beliebter Trick, um einem Parameter beim Übergeben in einem Methodenaufruf einen Namen zu geben.

Für die beiden anderen Layoutmanager trifft das gesagte ebenfalls zu. Einziger Unterschied ist hier, dass als erster Parameter nicht self, sondern natürlich der hauptFrame übergeben wird, den der soll sich ja um die beiden kümmern.


@leinwandFrame = FXVerticalFrame.new(
  @hauptFrame,
  LAYOUT_TOP|LAYOUT_LEFT|LAYOUT_FILL_X|LAYOUT_FILL_Y,
  ...

@menuFrame = FXVerticalFrame.new(
  @hauptFrame,
  LAYOUT_TOP|LAYOUT_LEFT|LAYOUT_FILL_Y,
  ...

Mehr Widgets, mehr Widgets

Jetzt schauen wir uns den linken und rechten Bereich genauer an. Im linken Leinwandbereich fügst du einen Text (Label), eine horizontale Trennlinie (Separator) und schließlich die Leinwand (Canvas) ein.


# * * * Linker Leinwandbereich
@leinwandFrame = FXVerticalFrame.new(
  @hauptFrame,
  LAYOUT_TOP|LAYOUT_LEFT|LAYOUT_FILL_X|LAYOUT_FILL_Y,
  :padLeft   => 20,
  :padRight  => 20,
  :padTop    => 20,
  :padBottom => 20
)

FXLabel.new(
  @leinwandFrame, 
  "Malbereich", 
  :opts => JUSTIFY_CENTER_X|LAYOUT_FILL_X
)

FXHorizontalSeparator.new(
  @leinwandFrame,
  SEPARATOR_GROOVE|LAYOUT_FILL_X
)

@leinwand = FXCanvas.new(
  @leinwandFrame,
  :opts => LAYOUT_FILL_X|LAYOUT_FILL_Y|LAYOUT_TOP|LAYOUT_LEFT
)

Das Label und den Separator legen wir blind an, d.h. wir legen für sie keine Instanzvariable an. Die Leinwand speichern wir hingegen in der Instanzvariablen @leinwand. Denn auf die Leinwand brauchen wir auch nach dem Erzeugen noch Zugriff.

Und nun noch den rechten Bereich.


# * * * Rechter Bereich für Buttons
@menuFrame = FXVerticalFrame.new(
  @hauptFrame,
  LAYOUT_TOP|LAYOUT_LEFT|LAYOUT_FILL_Y,
  :padLeft   => 20,
  :padRight  => 20,
  :padTop    => 20,
  :padBottom => 20
)

FXLabel.new(
  @menuFrame, 
  "Menü", 
  :opts => JUSTIFY_CENTER_X|LAYOUT_FILL_X
)

FXHorizontalSeparator.new(
  @menuFrame,
  SEPARATOR_GROOVE|LAYOUT_FILL_X
)

FXButton.new(
  @menuFrame,
  "&Beenden\tAnwendung beenden",
  nil,
  getApp(),
  FXApp::ID_QUIT,
  :opts => FRAME_THICK|FRAME_RAISED|LAYOUT_FILL_X|LAYOUT_TOP|LAYOUT_LEFT,
  :padLeft   => 10, 
  :padRight  => 10,
  :padTop    => 5,
  :padBottom => 5
)

Text und horizontale wie gehabt. Aber die Drucktaste (FXButton) sieht schon etwas komplizierter aus. Zuerst wieder der Layoutmanager, der sich um den Button kümmern soll, das ist klar.

Dann geht es aber schon los. Der zweite Parameter enthält den Text, der auf der Drucktaste erscheinen soll. Das &amp; bedeutet, dass der folgende Buchstabe unterstrichen dargestellt wird und der Button so bei gedrückter ALT-Taste mit diesem Buchstaben angewählt werden kann. In diesem Fall kannst du den Button nicht nur mit der Maus, sondern auch mit der Tastatur durch Drücken von ALT+B auslösen. Der Text ist aber noch nicht zu Ende. Es folgt abgetrennt mit einem Tabulatorzeichen \t ein weiterer Text, der als Tooltip für den Button verwendet wird (das funktioniert aber leider bei mir und bei dem Scribble Beispiel von FXRuby auch nicht).

Der dritte Parameter ist für ein Icon gedacht, da haben wir keins.

Der vierte Parameter gibt ein Objekt an, das beim Auslösen der Drucktaste irgendwie informiert werden soll. Und der fünfte Parameter gibt an, worüber dieses Empfängerobjekt informiert werden soll. In diesem Fall legen wir mit FXApp::ID_QUIT fest, dass der Empfänger (die Applikation selbst) ein QUIT, also ein Beenden von sich selbst einleiten soll.

Dann schauen wir uns das Ergebnis wieder einmal an.


[21.09.2008, 22:52]:> ruby lektion_22.rb

Bis auf den Leinwandbereich müsste nun auch bei dir alles wie gewünscht angezeigt werden. Aber warum wird der Leinwandbereich nicht gezeichnet? Stattdessen sieht man durch das Fenster hindurch auf die darunterliegenden Fenster. Irgendetwas scheint noch zu fehlen.

Die Leinwand (Canvas) ist ein ganz besonderes Widget. Es hat keine vorgegebene Gestalt. Es liegt bei uns, was dort angezeigt wird. Wir müssen es selbst dorthin malen. Wenn wir es nicht tun, dann bleiben die Pixel dort eben mit den Werten belegt, die sie vor dem Aufruf unserer Anwendung schon hatten.

Wir sollten also nun endlich entwas in die Canvas einzeichnen. Das Zeichnen bringen wir aber in einer separaten Methode unserer Hauptfensterklasse unter. Wir nennen sie onLeinwandRepaint und statten sie mit drei Übergabeparametern aus. Die Methode soll nämlich zu bestimmten Ereignissen aufgerufen werden, eben immer dann, wenn der Leinwandbereich neu gezeichnet werden muss. Die Bedeutung wird gleich noch etwas klarer. Hier zunächst die Malmethode für die Leinwand.


# In die Leinwand etwas einzeichnen
def onLeinwandRepaint(sender, sel, event)
  FXDCWindow.new(@leinwand, event) do |dc|
    dc.foreground = FXRGB(255, 255, 255)
    dc.fillRectangle(0, 0, @leinwand.width, @leinwand.height)

    # Einen LKW malen
    # Karosserie
    dc.foreground = FXColor::Blue
    dc.fillRectangle(30, 20, 30, 10) # Oberes Teil
    dc.fillRectangle(30, 30, 40, 10) # Unteres Teil

    # Seitenfenster
    dc.foreground = FXColor::DarkBlue
    dc.fillRectangle(32, 22, 15, 6) # Hinten
    dc.fillRectangle(48, 22, 10, 6) # Vorne

    # Mit Hintergrundfarbe einen Bereich für die 
    # Räder aus der Karosse schneiden
    dc.foreground = FXColor::White
    dc.fillCircle(38, 40, 5) # Hinten
    dc.fillCircle(62, 40, 5) # Vorne

    # Reifen anbringen
    dc.foreground = FXColor::Black
    dc.fillCircle(38, 40, 4) # Hinten
    dc.fillCircle(62, 40, 4) # Vorne

    # Felgen drüber malen
    dc.foreground = FXColor::White
    dc.fillCircle(38, 40, 2) # Hinten
    dc.fillCircle(62, 40, 2) # Vorne

    # Rücklicht
    dc.foreground = FXColor::Red
    dc.fillRectangle(29, 30, 2, 6)

    # Blinklicht vorne
    dc.foreground = FXColor::DarkOrange
    dc.fillRectangle(67, 32, 3, 2)
  end
end

Wir malen einen weißen Hintergrund mit einem bunten Lastkraftwagen. Das Malen erfolgt über den sogenannten Device Context (Gerätekontext), daher die Abkürzung dc für die Variable. Dieser Gerätekontext repräsentiert im Prinzip den Monitor, oder genauer gesagt nur den Bereich des Monitors, der von unserer Leinwand ausgefüllt wird. An diesen Gerätekontext können wir nun Zeichenbefehle absetzen, die dann am Monitor ankommen. Wir können umrandete Rechtecke (drawRectangle) oder ausgefüllte Rechtecke (fillRectangle) zeichnen, oder auch Kreise (drawCircle bzw. fillCircle). Natürlich auch Punkte (drawPoint), Linien (drawLine) und Polygone (fillPolygon) und einiges mehr.

Den LKW setzen wir nur mit Rechtecken und Kreisen zusammen. Die Farbe wechseln wir dabei an passender Stelle, indem wir die Vordergrundfarbe des Gerätekontextes ändern (dc.foreground). Für die Farben verwenden wir vordefinierte Werte aus der Klasse FXColor, aber man kann auch mit FXRGB innerhalb des RGB Farbraums beliebig eine eigene Farbe auswählen.

Die Methode zum Bemalen des Leinwandbereiches ist nun fertig, aber es funktioniert immer noch nicht. FXRuby weiß ja auch noch nicht, wann es diese Methode aufrufen soll.

Die Application überwacht die gesamte Anwendung, das weißt du aus der Einleitung in der letzten Lektion. Stellt die Anwendung fest, dass ein bestimmtes Widget neu gezeichnet werden muss, weil es bspw. bis jetzt von einem anderen Fenster verdeckt war, so schickt es an das Widget eine Nachricht. Die Nachricht wird in FXRuby als Selektor bezeichnet. Soll ein Widget sich selbst neu zeichnen, so lautet der Selektor SEL_PAINT. Wir müssen der Leinwand also noch sagen, dass sie immer auf diese Nachrichten achten soll. Dazu verbinden wir an der Leinwand den Selektor SEL_PAINT mit der Methode onLeinwandRepaint mit folgendem Methodenaufruf am Leinwandobjekt:


@leinwand.connect(
  SEL_PAINT, 
  method(:onLeinwandRepaint)
)

Nun ruft die Leinwand immer dann, wenn sie sich selbst neu zeichnen soll, die Methode onLeinwandRepaint(sender, sel, event) mit sich selbst als Sender auf.

Hier nun der komplette Code und das Ergebnis beim Aufruf (wie immer auch im Downloadbereich).


# Copyright (C) 2007-2008 www.rubykids.de Frithjof Eckhardt
# Alle Rechte vorbehalten.
# lektion_22.rb

require 'fox16'
require 'fox16/colors'

include Fox

class RubykidsMainWindow < FXMainWindow
  def initialize(app)
    super(app, "Rubykids.de", :opts => DECOR_ALL, :width => 800, :height => 600)

    @hauptFrame = FXHorizontalFrame.new(
      self,
      LAYOUT_SIDE_TOP|LAYOUT_FILL_X|LAYOUT_FILL_Y,
      :padLeft   => 0,
      :padRight  => 0,
      :padTop    => 0,
      :padBottom => 0
    )

    # * * * Linker Leinwandbereich
    @leinwandFrame = FXVerticalFrame.new(
      @hauptFrame,
      LAYOUT_TOP|LAYOUT_LEFT|LAYOUT_FILL_X|LAYOUT_FILL_Y,
      :padLeft   => 20,
      :padRight  => 20,
      :padTop    => 20,
      :padBottom => 20
    )

    FXLabel.new(
      @leinwandFrame, 
      "Malbereich", 
      :opts => JUSTIFY_CENTER_X|LAYOUT_FILL_X
    )

    FXHorizontalSeparator.new(
      @leinwandFrame,
      SEPARATOR_GROOVE|LAYOUT_FILL_X
    )

    @leinwand = FXCanvas.new(
      @leinwandFrame,
      :opts => LAYOUT_FILL_X|LAYOUT_FILL_Y|LAYOUT_TOP|LAYOUT_LEFT
    )

    @leinwand.connect(
      SEL_PAINT, 
      method(:onLeinwandRepaint)
    )

    # * * * Rechter Bereich für Buttons
    @menuFrame = FXVerticalFrame.new(
      @hauptFrame,
      LAYOUT_TOP|LAYOUT_LEFT|LAYOUT_FILL_Y,
      :padLeft   => 20,
      :padRight  => 20,
      :padTop    => 20,
      :padBottom => 20
    )

    FXLabel.new(
      @menuFrame, 
      "Menü", 
      :opts => JUSTIFY_CENTER_X|LAYOUT_FILL_X
    )

    FXHorizontalSeparator.new(
      @menuFrame,
      SEPARATOR_GROOVE|LAYOUT_FILL_X
    )

    FXButton.new(
      @menuFrame,
      "&Beenden\tAnwendung beenden",
      nil,
      getApp(),
      FXApp::ID_QUIT,
      :opts => FRAME_THICK|FRAME_RAISED|LAYOUT_FILL_X|LAYOUT_TOP|LAYOUT_LEFT,
      :padLeft   => 10, 
      :padRight  => 10,
      :padTop    => 5,
      :padBottom => 5
    )
  end

  # In die Leinwand etwas einzeichnen
  def onLeinwandRepaint(sender, sel, event)
    FXDCWindow.new(@leinwand, event) do |dc|
      dc.foreground = FXRGB(255, 255, 255)
      dc.fillRectangle(0, 0, @leinwand.width, @leinwand.height)

      # Einen LKW malen
      # Karosserie
      dc.foreground = FXColor::Blue
      dc.fillRectangle(30, 20, 30, 10) # Oberes Teil
      dc.fillRectangle(30, 30, 40, 10) # Unteres Teil

      # Seitenfenster
      dc.foreground = FXColor::DarkBlue
      dc.fillRectangle(32, 22, 15, 6) # Hinten
      dc.fillRectangle(48, 22, 10, 6) # Vorne

      # Mit Hintergrundfarbe einen Bereich für die 
      # Räder aus der Karosse schneiden
      dc.foreground = FXColor::White
      dc.fillCircle(38, 40, 5) # Hinten
      dc.fillCircle(62, 40, 5) # Vorne

      # Reifen anbringen
      dc.foreground = FXColor::Black
      dc.fillCircle(38, 40, 4) # Hinten
      dc.fillCircle(62, 40, 4) # Vorne

      # Felgen drüber malen
      dc.foreground = FXColor::White
      dc.fillCircle(38, 40, 2) # Hinten
      dc.fillCircle(62, 40, 2) # Vorne

      # Rücklicht
      dc.foreground = FXColor::Red
      dc.fillRectangle(29, 30, 2, 6)

      # Blinklicht vorne
      dc.foreground = FXColor::DarkOrange
      dc.fillRectangle(67, 32, 3, 2)
    end
  end

  # Wird von application.create aufgerufen
  def create
    super
    show(PLACEMENT_SCREEN)
  end

end

application = FXApp.new
    mainWin = RubykidsMainWindow.new(application)
application.create
application.run

Wie die X- und Y-Koordinatenwerte für den LKW gewählt wurden, geht aus der folgenden kleinen Skizze etwas anschaulicher hervor:

Peter und Livia

Peter: Fährt das Ding auch? Livia: Bisher nicht, aber mach’ weiter schön mit, dann fährt es vielleicht.

Peter: Auch rückwärts? Livia: Auch um die Kurve!

Meine Nachricht

Einen Kommentar hinterlassen

  1. thopre@gmail.com 12 months later:
    übrigens läuft dieses Beispiel nicht mehr mit Ruby 1.9. Abhilfe: # encoding: utf-8 in die erste Zeile schreiben