QVINTVS · SCRIBET

wxRuby-Programmierung, Teil III: Sizers

Sizers sind ein elementarer Bestandteil der GUI-Programmierung mit wxRuby. Grund genug, da mal einen Blick drauf zu werfen.

Starten wir einfach mit der Anwendung, die wir beim letzten mal programmiert haben:

# -*- coding: utf-8 -*-

require "wx"

class MainFrame < Wx::Frame
  include Wx

  def initialize(parent = nil)
    super(parent, title: "Hello, world!", size: [400, 400])
    create_widgets
    setup_event_handlers
  end

  private

  def create_widgets
    @text_ctrl   = TextCtrl.new(self, pos: [20, 20], size: [200, -1])
    @save_button = Button.new(self, pos: [240, 20], label: "Save")
    @show_button = Button.new(self, pos: [20, 50], label: "Show")
  end

  def setup_event_handlers
    evt_button(@save_button, :on_save_button_clicked)
    evt_button(@show_button, :on_show_button_clicked)
  end

  # Event handling

  def on_save_button_clicked(event)
    @name = @text_ctrl.value
  end

  def on_show_button_clicked(event)
    md = MessageDialog.new(self, 
                           caption: "Your name", 
                           message: "Your name is: #{@name}",
                           style: ICON_INFORMATION | OK)
    md.show_modal
  end

end

class MyApp < Wx::App

  def on_init
    @mainwindow = MainFrame.new
    @mainwindow.show
  end

end

MyApp.new.main_loop

Diesmal liegt unser Augenmerk darauf, die lästigen Positionsberechnungen loszuwerden und außerdem ein dynamisches Layout zu schaffen. Wer das Fenster etwa mal vergrößert und verkleinert hat, dürfte festgestellt haben, dass das Layout darauf in keinster Weise reagiert, was für Anwendungen, die maximiert werden sollen, denkbar schlecht ist. wxRuby bietet für solche Dinge Objekte namens Sizers an.

Was sind Sizers?

Ein Sizer lässt sich vom Prinzip her mit einem Spanngurt vergleichen: Man macht ihn an zwei Seiten fest, und er nimmt den gesamten Raum zwischen diesen beiden Punkten ein, etwa so:

|(================================)|

Wenn man nun z.B. auf der rechten Seite etwas zwischen Wand und Gurt packt, zieht der Gurt sich zusammen und nimmt nur noch den verbliebenen Raum ein.

|(==============================)##|

Ich sehe ein, das Beispiel hinkt etwas, aber mir fiel nichts besseres ein. Die korrekte Darstellung des zusammengezogenen Sizers sähe so aus:

|(==============================##)|

Denn in wxRuby “befüllt” man Sizers und packt sie nicht irgendwo zwischen. Neben den sich verkleinernen Sizers gibt es auch noch welche, die sich genau umgekehrt verhalten: Sie versuchen, soviel Raum wie möglich zu bekommen.

Vorüberlegungen

Die in wxRuby am häufigsten gebrauchte Sizer-Klasse ist Wx::BoxSizer, genau genommen Wx::HBoxSizer für horizontale “Spanngurte” und Wx::VBoxSizer für vertikale (beides Subklassen von Wx::BoxSizer). Baut man eine GUI, ist es also sinnvoll, sich zunächst zu überlegen, ob die einzelnen Widgets horizontal oder vertikal voneinander abhängen. Die oberste Ebene der GUI (Sizer werden fürgewöhnlich verschachtelt, um den gewünschten Effekt zu erreichen) ist in der Regel vertikal orientiert, d.h. die Widgets befinden sich untereinander. Hat man aber ein Eingabefeld mit einem Button rechts daneben (so wie wir oben), ist dies eine horizontale Beziehung innerhalb einer Zeile des vertikalen Sizers. Skizziert man die vorhandene GUI mithilfe von Sizers, so könnte etwa das folgende Layout entstehen:

↑
| (Textfeld) ←--→ (Button)
| (Leer)
| (Button)
↓

Umsetzung

Diese Skizze gilt es nun in Sizer-Code umzusetzen. Der erste Schritt dabei ist es, die expliziten Positionsangaben aus den new-Aufrufen zu entfernen (wir lassen ja schließlich wxRuby rechnen):

def create_widgets
  @text_ctrl   = TextCtrl.new(self)
  @save_button = Button.new(self, label: "Save")
  @show_button = Button.new(self, label: "Show")
end

Um das Layout von der Logik zu trennen, führen wir eine neue Methode ein, die ebenfalls aus initialize heraus aufgerufen wird:

def initialize(parent = nil)
  super(parent, title: "Hello, world!", size: [400, 400])
  create_widgets
  make_layout
  setup_event_handlers
end

#...

def make_layout

end

Achtet bitte darauf, die Methode nicht layout zu nennen, denn es gibt schon eine Methode Wx::Window#layout, die schon von wxRuby belegt ist und nicht überschrieben werden sollte.

Sizers!

Fangen wir mit dem obersten, dem vertikal orientierten, Sizer an.

def make_layout
  @top_sizer = VBoxSizer.new
  self.sizer = @top_sizer
end

Es ist wohl nicht zwangsweise notwendig, den Sizer in einer Instanzvariablen zu speichern, aber ich persönlich halte das der Übersicht wegen für besser. Wer mag, kann den Sizer auch direkt bei self.sizer setzen oder in einer lokalen Variable speichern (wir brauchen ihn nämlich gleich wieder). Die schon angesprochene zweite Zeile teilt wxRuby mit, dass dieses Fenster nicht manuell, sondern mithilfe eines Sizers verwaltet werden soll und welcher Sizer das denn sein soll. wxRuby benutzt diesen “obersten” Sizer als Grundlage für alle folgenden Layout-Berechnungen.

Jetzt haben wir also einen Sizer, der so klein wie möglich zu sein versucht und neue Elemente vertikal ausrichtet, d.h. würden wir Widgets an den Sizer anfügen, käme soetwas dabei heraus:

↑
(Widget1)
(Widget2)
(Widget3)
↓

Genau das machen wir jetzt auch, indem wir den show_button hinzufügen:

  def make_layout
    @top_sizer = VBoxSizer.new
    self.sizer = @top_sizer

    @top_sizer.add_item(@show_button)
  end

Man erkennt, neue Elemente werden mithilfe von Sizer#add_item an einen Sizer angefügt. Das ist aber noch längst nicht alles, es gilt, auch das Textfeld und rechts neben ihm den zweiten Button hinzuzufügen — das ist jedoch nicht ganz so einfach. Schaut man sich die weiter oben verfertigte Skizze an, so stellt man fest, dass sich Speicherbutton und Textfeld in einer horizontalen Beziehung befinden. Wir brauchen demnach einen horizontalen Sizer, arbeiten aber momentan mit einem Vertikalen. Was also tun? Ganz einfach: Einfach einen horizontalen Sizer zum Vertikalen hinzufügen!

  def make_layout
    @top_sizer = VBoxSizer.new
    self.sizer = @top_sizer

    sizer = HBoxSizer.new
    sizer.add_item(@text_ctrl)
    sizer.add_item(@save_button)
    @top_sizer.add_item(sizer)

    @top_sizer.add_item(@show_button)
  end

Sizers zu verschachteln ist nichts unübliches in wxRuby, man muss lediglich darauf achten, nicht den Überblick zu verlieren1. Wer diese GUI startet, wird ein ziemlich eng gedrängtes Layout sehen — wie schon gesagt, versuchen Sizers standardmäßig sich spanngurtartig so klein wie irgend möglich zusammenzuziehen.

Um die Sache aufzulockern, benötigen wir etwas: Eine Möglichkeit, aus einem Schrumpf- einen Wachs-Sizer zu machen. In wxRuby schaut das so aus:

  def make_layout
    @top_sizer = VBoxSizer.new
    self.sizer = @top_sizer

    sizer = HBoxSizer.new
    sizer.add_item(@text_ctrl, proportion: 1)
    sizer.add_item(@save_button)
    @top_sizer.add_item(sizer, flag: EXPAND)

    @top_sizer.add_item(@show_button)
  end

Fügt man ein Widget oder einen Untersizer zu einem Sizer hinzu, so kann man Flags übergeben, von denen eine Wx::EXPAND (gleichbedeutend mit Wx::GROW) ist. Sie weist das hinzugefügte Element an, sich nicht nach der so-klein-wie-möglich-Variante zu verhalten, sondern eben genau umgekehrt; das Element versucht, soviel Platz wie möglich einzunehmen.

Das gilt aber nicht rekursiv, d.h. die Elemente, die dem HBox-Sizer hinzugefügt werden, zeigen wieder das Minimal-Verhalten. Damit der neue Platz auch genutzt wird, bedienen wir uns eines weiteren Parameters namens proportion. Er gibt an, in welchem Verhältnis der zur Verfügung stehende Platz verteilt wird (standardmäßig haben alle Elemente eine proportion von 0). Hat man etwa zwei Widgets, von denen das erste eine Proportion von 1 hat, das zweite aber eine von 2, erhält man ein Verhältnis von 1:2, bei dem das zweite Widget doppelt soviel Platz einnehmen wird wie das erste. Man kann jedoch auch wesentlich kompliziertere Verhältnisse wählen, z.B. 23:11 und ist natürlich nicht nur auf zwei Widgets beschränkt, auch Verhältnisse wie 2:1:1 sind möglich, bei dem das erste Widget doppelt so groß würde wie das jeweils zweite und dritte².

Um schlussendlich noch den Show-Button ans untere Ende der GUI zu verschieben, benötigen wir ein Füllelement, dass wir zwischen den Untersizer und den Button packen. Idealerweise sollte dieses Füllelement sich so groß wie möglich machen und sich den Umstängen anpassen — in wxRuby gibt es für solche zwecke Spacers, genauer genommen sogenannte Stretch Spacers (normale Spacers kann man nur auf eine fixe Pixelbreite/-höhe einstellen). Indem wir einen solchen zwischen den Show-Button und den Kindsizer einfügen, erhalten wir das gewünschte Layout:

  def make_layout
    @top_sizer = VBoxSizer.new
    self.sizer = @top_sizer

    sizer = HBoxSizer.new
    sizer.add_item(@text_ctrl, proportion: 1)
    sizer.add_item(@save_button)
    @top_sizer.add_item(sizer, flag: EXPAND)

    @top_sizer.add_stretch_spacer

    @top_sizer.add_item(@show_button)
  end

Sizers können aber noch mehr. Es handelt sich bei ihnen um ziemlich vielseitig verwendbare Layout-Elemente und nach dieser Einführung empfiehlt es sich, mal einen Blick auf ihre Dokumentation zu werfen. Es gibt auch noch mehr als nur die hier vorgestellten HBox- und VBox-Sizers, etwa einen Grid-Sizer für ein Tabellenlayout, aber die hier vorgestellten Sizers und Layout-Techniken sind die am häufigsten angewandten. Mit ihnen als Startpunkt sollte es nicht mehr allzu schwer sein, die Dokumentation zu verstehen.

Zum Schluss noch den vollständigen Code des Testprogramms, das nun vollständig auf Größenveränderungen reagiert; probiert es ruhig einmal aus, vergrößert, verkleinert, maximiert und minimiert — die GUI denkt mit!

# -*- coding: utf-8 -*-

require "wx"

class MainFrame < Wx::Frame
  include Wx

  def initialize(parent = nil)
    super(parent, title: "Hello, world!", size: [400, 400])
    create_widgets
    make_layout
    setup_event_handlers
  end

  private

  def create_widgets
    @text_ctrl   = TextCtrl.new(self)
    @save_button = Button.new(self, label: "Save")
    @show_button = Button.new(self, label: "Show")
  end

  def make_layout
    @top_sizer = VBoxSizer.new
    self.sizer = @top_sizer

    sizer = HBoxSizer.new
    sizer.add_item(@text_ctrl, proportion: 1)
    sizer.add_item(@save_button)
    @top_sizer.add_item(sizer, flag: EXPAND)

    @top_sizer.add_stretch_spacer

    @top_sizer.add_item(@show_button)
  end

  def setup_event_handlers
    evt_button(@save_button, :on_save_button_clicked)
    evt_button(@show_button, :on_show_button_clicked)
  end

  # Event handling

  def on_save_button_clicked(event)
    @name = @text_ctrl.value
  end

  def on_show_button_clicked(event)
    md = MessageDialog.new(self, 
                           caption: "Your name", 
                           message: "Your name is: #{@name}",
                           style: ICON_INFORMATION | OK)
    md.show_modal
  end

end

class MyApp < Wx::App

  def on_init
    @mainwindow = MainFrame.new
    @mainwindow.show
  end

end

MyApp.new.main_loop

Valete.


1 Das Gem wx_sugar, welches Erweiterungen für wxRuby bereitstellt, erlaubt es, Sizer-Verschachtelungen mithilfe von Blöcken zu realisieren. Ich habe das aber bislang noch nicht gebraucht. Außerdem ist wx_sugar ziemlich fett, und wenn man nur dieses eine Feature will…

2 Es ist übrigens meines Wissens nach nicht möglich, für Widgets einfach die Wx::EXPAND-Flag zu übergeben, man muss mit proportion arbeiten. Wieso ist mir noch nicht ganz klar. Wx::EXPAND scheint nur für Sizers zu funktionieren.