Ein HelloWorld mit CreateFrame()





  • Level: AddOn Profi
  • Tools: XML Lua-Editor, Original Interfacedateien von WoW
  • Zeitaufwand: ca. 25 min

Motivation

Mit Patch 1.10 hat Blizzard den AddOn-Entwicklern einen lange gehegten Wunsch erfüllt: Die Möglichkeit UI-Elemente dynamisch durch Lua-Code zu erzeugen. Bisher galt der Grundsatz: Alles was auf dem UI sichtbar ist, muss vorher in einer XML-File als Element deklariert werden. Die Nachteile dieser Methode liegen auf der Hand:
  • Die genaue Zahl der verfügbaren UI-Elemente wird zur Compile-Zeit festgelegt. Beispiel: Flexbar. Das AddOn besitzt von Haus aus 120 Buttons, obwohl wahrscheinlich die allerwenigsten Spieler jemals so viele Knöpfe benutzen werden. Andererseits kann es genausogut sein, dass ein Spieler gerade 125 Buttons für sein perfektes UI braucht. Diese Problematik ist in dem statischen Pre-1.10-System nicht lösbar.
  • Das Hinzufügen neuer UI-Elemente verlangt zwingend das Erstellen einer XML-Datei. Insbesondere ist es also nicht möglich, UI-Elemente nur durch ein Makro zu erstellen
  • Das Recyclen von UI-Elementen gestaltet sich als enorm schwierig. Nehmen wir an, unser AddOn begrüßt den Benutzer mit einem Hello!-Knopf, der nur einmal beim Start gezeigt werden soll. Sobald der Spieler den Button weggeklickt hat, lungert dieser nutzlos im Speicher herum. Mit den neuen 1.10-Features können wir diesen Button sobald er nicht mehr benötigt wird an anderer Stelle wiederverwenden und auf diese Weise Ressourcen und Code sparen.


Erste Schritte

Für dieses HowTo werden wir einen normalen Knopf erstellen, basierend auf dem GameMenuButtonTemplate von Blizzard. Dieses Beispiel eignet sich nicht nur hervorragend als Anschauung für die Benutzung von CreateFrame(), sondern bietet außerdem auch einen Blick in die Tücken und Kinderkrankheiten dieser noch jungen Technik. Ausgangspunkt unserer Reise ist folgender XML-Code:

  1. Button name=”my_Button” inherits=”GameMenuButtonTemplate” parent=”UIParent”
  2. Size AbsDimension x=”100″ y=”50″/
  3. /Size Anchors Anchor point=”CENTER”/
  4. /Anchors Scripts OnClick DEFAULT_CHATFRAME:AddMessage(“Hello World!”);
  5. /OnClick
  6. /Scripts
  7. /Button

Unser Ziel ist ein Block Lua-Code, der einen identischen Knopf wie dieser XML-Block erstellt. Na, das kann ja so schwer gar nicht werden… ;)
Aber unerwarteterweise finden wir eine der größten Hürden bereits in der ersten Zeile unseres XML-Codes: CreateFrame() erlaubt in seiner jetzigen Form keine Verwendung von Templates!
Der Grund dafür ist denkbar einfach: WoW löst sämtliche Verweise auf Templates bereits beim Laden des UIs auf, die Templates selbst werden niemals als Objekte im Speicher erstellt! Darüberhinaus akzeptiert CreateFrame() aber auch gar keinen Parameter von dem man Elementeigenschaften ableiten könnte. Unsere Aufgabe ist es nun also, die Templateverweise von Hand aufzulösen. Ein Blick in die UIPanelTemplates.XML von Blizzard verrät uns, dass GameMenuButtonTemplate seinerseits wieder von UIPanelButtonTemplate ableitet. Dieses Template verwendet selbst jedoch auch wieder Templates für Schriftarten und Texturen… Kurz gesagt: Ein heilloses Durcheinander :o
Der vollständig aufgelöste XML-Code sieht so aus:

  1. Button name=”my_Button” parent=”UIParent”
  2. Size AbsDimension x=”100″ y=”50″/
  3. /Size Anchors Anchor point=”CENTER” relativeTo=”UIParent”
  4. relativePoint=”CENTER”/ /Anchors !-NormalText inherits=”GameFontHighlight”/ – NormalText
  5. font=”FontsFRIZQT__.TTF” FontHeight AbsValue val=”12″/
  6. /FontHeight Color r=”1.0″ g=”1.0″ b=”1.0″/
  7. Shadow Offset AbsDimension x=”1″ y=”-1″/
  8. /Offset Color r=”0″ g=”0″ b=”0″/
  9. /Shadow
  10. /NormalText !-DisabledText inherits=”GameFontDisable”
  11. /- DisabledText font=”FontsFRIZQT__.TTF” FontHeight AbsValue val=”12″/
  12. /FontHeight Color r=”0.5″ g=”0.5″ b=”0.5″/
  13. Shadow Offset AbsDimension x=”1″ y=”-1″/
  14. /Offset Color r=”0″ g=”0″ b=”0″/
  15. /Shadow
  16. /DisabledText !-HighlightText inherits=”GameFontHighlight”/- HighlightText
  17. font=”FontsFRIZQT__.TTF” FontHeight AbsValue val=”12″/
  18. /FontHeight Color r=”1.0″ g=”1.0″ b=”1.0″/
  19. Shadow Offset AbsDimension x=”1″ y=”-1″/
  20. /Offset Color r=”0″ g=”0″ b=”0″/
  21. /Shadow
  22. /HighlightText NormalTexture file=”InterfaceButtonsUI-Panel-Button-Up” TexCoords left=”0″
  23. right=”0.625″ top=”0″ bottom=”0.6875″/
  24. /NormalTexture PushedTexture file=”InterfaceButtonsUI-Panel-Button-Down”
  25. TexCoords left=”0″ right=”0.625″ top=”0″ bottom=”0.6875″/
  26. /PushedTexture DisabledTexture file=”InterfaceButtonsUI-Panel-Button-Disabled”
  27. TexCoords left=”0″ right=”0.625″ top=”0″ bottom=”0.6875″/
  28. /DisabledTexture HighlightTexture file=”InterfaceButtonsUI-Panel-Button-Highlight”
  29. alphaMode=”ADD” TexCoords left=”0″ right=”0.625″ top=”0″ bottom=”0.6875″/
  30. /HighlightTexture Scripts OnClick DEFAULT_CHATFRAME:AddMessage(“Hello World!”);
  31. /OnClick
  32. /Scripts
  33. /Button

Versucht diese änderung anhand von Blizzards UI-Dateien selbst nachzuvollziehen. Das Auflösen von Templates ist nicht schwer, aber es stellt ein unverzichtbares Werkzeug im Umgang mit CreateFrame() dar!

Der Sprung zu Lua

Alles was jetzt noch zu tun bleibt, ist diesen unübersichtlichen XML-Klotz in die neuen Lua-Funktionen zu pressen. Den Anfang macht dabei unser heißgeliebtes CreateFrame():

  1. CreateFrame ( “Button”, “my_DynButton”, UIParent ) ;

Der erste Parameter gibt den Typ des zu erstellenden Objekts an, in unserem Fall ein Knopf. Der zweite Parameter ist optional und gibt den Namen des zu erstellenden Objekts an. So könnte man zum Beispiel durch Aufruf von local my_DynButton = CreateFrame(“Button”, UIParent); verhindern, dass der Knopf im globalen Scope auftaucht. Der dritte Parameter ist ebenfalls optional und gibt das Elternobjekt an. Dieser Parameter kann allerdings nicht verwendet werden, um Objekteigenschaften zu vererben! Er dient lediglich der internen Hierarchisierung. Als brave Programmierer, die sich immer an Konventionen halten *hust* geben wir hier UIParent an.
Was tut dieser Aufruf von CreateFrame() jetzt genau? Grob gesagt, er allokiert den benötigten Speicher und registriert unseren Button im UI-System des Clients. Wir erhalten einen Blanko-Button als Resultat, ein unbeschriebenes Objekt, das erst noch mit Leben gefüllt werden muss.
Als nächstes setzen wir einen Anchorpoint und die Größe des Buttons fest, denn ohne diese Informationen kann das UI den Button unmöglich darstellen:

  1. my_DynButton:SetPoint ( “CENTER”, “UIParent”, “CENTER”, 0, 0 ) ;
  2. my_DynButton:SetHeight ( 50 ) ;
  3. my_DynButton:SetWidth ( 100 ) ;

Als nächstes sind die verschiedenen Texturen an der Reihe. Hierzu benötigen wir eine weitere Funktion, die neu in 1.10 ist, Frame:CreateTexture(). Diese Funktion bekommt als optionale Parameter den Namen der Textur und den Layer in dem die Texture erstellt werden soll. Letzteres ist für unsere Zwecke nicht relevant und damit ich mir nicht noch mehr bescheuerte Namen mit dämlichen Präfixen ausdenken muss, verzichte ich auch gleich auf den ersten. Beachtet aber, dass es bei meiner Methode enorm schwierig ist, die Textur im Nachhinein zu ändern, da sie ja nicht benannt wird. Wenn ihr also euren Button später recyclen wollt, stellt sicher, dass ihr entweder den Namen der Texturelemente kennt, oder zumindest eine Referenz darauf abgespeichert habt.

  1. local mytex = my_DynButton:CreateTexture ( ) ;
  2. mytex:SetTexture ( “Interface \Buttons \UI-Panel-Button-Up” ) ;
  3. mytex:SetTexCoord ( 0, 0.625, 0, 0.6875 ) ;
  4. mytex:SetPoint ( “CENTER”, “UIParent”, “CENTER”, 0, 0 ) ;
  5. mytex:SetHeight (my_DynButton:GetHeight ( ) ) ;
  6. mytex:SetWidth (my_DynButton:GetWidth ( ) ) ;
  7. my_DynButton:SetNormalTexture (mytex ) ;
  8. mytex = my_DynButton:CreateTexture ( ) ;
  9. mytex:SetTexture ( “Interface \Buttons \UI-Panel-Button-Down” ) ;
  10. mytex:SetTexCoord ( 0, 0.625, 0, 0.6875 ) ;
  11. mytex:SetPoint ( “CENTER”, “UIParent”, “CENTER”, 0, 0 ) ;
  12. mytex:SetHeight (my_DynButton:GetHeight ( ) ) ;
  13. mytex:SetWidth (my_DynButton:GetWidth ( ) ) ;
  14. my_DynButton:SetPushedTexture (mytex ) ;
  15. mytex = my_DynButton:CreateTexture ( ) ;
  16. mytex:SetTexture ( “Interface \Buttons \UI-Panel-Button-Disabled” ) ;
  17. mytex:SetTexCoord ( 0, 0.625, 0, 0.6875 ) ;
  18. mytex:SetPoint ( “CENTER”, “UIParent”, “CENTER”, 0, 0 ) ;
  19. mytex:SetHeight (my_DynButton:GetHeight ( ) ) ;
  20. mytex:SetWidth (my_DynButton:GetWidth ( ) ) ;
  21. my_DynButton:SetDisabledTexture (mytex ) ;
  22. mytex = my_DynButton:CreateTexture ( ) ;
  23. mytex:SetTexture ( “Interface \Buttons \UI-Panel-Button-Highlight” ) ;
  24. mytex:SetTexCoord ( 0, 0.625, 0, 0.6875 ) ;
  25. mytex:SetPoint ( “CENTER”, “UIParent”, “CENTER”, 0, 0 ) ;
  26. mytex:SetHeight (my_DynButton:GetHeight ( ) ) ;
  27. mytex:SetWidth (my_DynButton:GetWidth ( ) ) ;
  28. my_DynButton:SetHighlightTexture (mytex ) ;

Ich verzichte an dieser Stelle der Einfachheit halber auf das Setzen des Blendingmodes der Highlight-Textur. Das Ergebnis ist optisch identisch mit unserem Referenzbutton.

Der letzte Schliff und erste Probleme

Weils so schön einfach ist, bauen wir an dieser Stelle den OnClick-Eventhandler ein. Die Funktionen dafür sind ja bereits seit Patch 1.7 implementiert:

  1. local tfun = function ( ) DEFAULT_CHATFRAME:AddMessage ( “Hello World!” ) ;
  2. end my_DynButton:SetScript ( “OnClick”, tfun ) ;

Das einzige was jetzt noch fehlt, sind die unterschiedlichen Fonts für NormalText, DisabledText und HighlightText. Auch hierzu stellt Blizzard wieder eine Reihe von Methoden bereit:

  1. local fo = CreateFont ( ) ;
  2. fo:SetFont ( “Fonts \FRIZQT__.TTF”, 12 ) ;
  3. fo:SetTextColor ( 1.0, 1.0, 1.0, 1.0 ) ;
  4. fo:SetShadowOffset ( 1, - 1 ) ; fo:SetShadowColor ( 0.0, 0.0, 0.0, 0.0 ) ;
  5. my_DynButton:SetTextFontObject (fo ) ;
  6. my_DynButton:SetDisabledFontObject (fo ) ;
  7. my_DynButton:SetHighlightFontObject (fo ) ;

Der Einfachheit halber habe ich nur ein FontObject für alle drei Schriftarten verwendet.
Beachtet hierbei, dass WoW mit 1.10 sein Schriftartensystem umgestellt hat! Statt wie bisher FontString sowohl für Schriftsätze im UI als auch zur Beschreibung von Schriftarten zu verwenden, gibt es nun zwei Objekttypen, Font und FontString. Während ersterer alle Informationen über die Schriftart enthält (verwendete Schriftdatei, Größe, Farbe, etc), wird FontString nun ausschließlich dazu verwendet, “sichtbare” Schriftzüge im UI zu erstellen. Insbesondere ist es nicht länger möglich, Informationen zur Schriftart direkt im FontString Objekt abzulegen!
Damit wäre unser Knopf dann auch schon fertig. Also, WoW laden und los geht der Spaß! Aber halt, was ist das? Unser Button sträubt sich vehement gegen jede Form von Beschriftung. Probiert man folgenden Code aus:

  1. my_DynButton:SetText ( “Hallo Welt!” ) ; local text = my_DynButton:GetText ( ) ; DEFAULT_CHATFRAME:AddMessage ( type (text ) ) ;

So bekommt man lediglich ein sträfliches nil in den Chat ausgegeben. Damit wären wir auch schon bei den eingangs erwähnten Kinderkrankheiten ;) Momentan funktioniert die Beschriftung von Buttons nicht. Bis dieser Bug von Blizzard behoben wird, bleibt dem ambitionierten AddOn-Entwickler nichts anderes übrig, als auf einen Workaround auszuweichen.

Flickwerkzeug

Die Idee ist folgende: Wir erstellen einen FontString, der bündig über den eigentlichen Button drübergelegt wird. Auf diese Weise macht es für den Benutzer den Eindruck, es handle sich um einen ganz gewöhnlichen, beschrifteten Knopf, in Wahrheit haben wir es jedoch mit zwei Objekten zu tun. Wir erstellen also einen FontString als Kindobjekt zu unserem Knopf:

  1. mytext = my_DynButton:CreateFontString ( ) ;
  2. mytext:SetFont ( “Fonts \FRIZQT__.TTF”, 12 ) ;
  3. mytext:SetPoint ( “CENTER”, ( (mytext:GetParent ( ) ):GetName ( ) ), “CENTER”, 0, 0 ) ;
  4. mytext:SetWidth ( ( (mytext:GetParent ( ) ):GetWidth ( ) ) ) ;
  5. mytext:SetHeight ( ( (mytext:GetParent ( ) ):GetHeight ( ) ) ) ;

Beachtet, dass dies folgendem XML Code entspricht:

  1. Button name=”my_Button” …
  2. … Frames FontString …/
  3. /Frames /Button

Die Nachteile dieser Methode sind klar: Wollen wir die Beschriftung des Buttons ändern, müssen wir nun statt my_DynButton:SetText() die Methode mytext:SetText() aufrufen. Folglich müssen wir entweder eine Referenz auf mytext abgespeichert haben, oder diese zumindest zur Laufzeit wiederfinden können. Eine dritte Alternative wäre das überladen der my_DynButton:SetText()-Methode, wobei das Hooken von Objektmethoden nicht ganz trivial ist und einige Besonderheiten mit sich bringt. Kurz gesagt: Egal wie mans dreht und wendet, die Sache ist immer irgendwie Murks.
Als Beispiel sei hier der zweite Ansatz aufgeführt, da dieser die ebenfalls mit 1.10 eingeführte Methode GetRegions() verwendet:

  1. local regions = { my_DynButton:GetRegions ( ) } ;
  2. for i = 1, my_DynButton:GetNumRegions ( ) do if (regions:GetObjectType ( ) == “FontString” ) then regions:SetText ( “Hallo Welt!” ) ;
  3. end end

Ein wenig quick”n”dirty, aber ich hoffe, die Idee ist klar geworden. Eine Sache noch, bevor wir Schluss machen: Es ist nicht möglich, mit CreateFrame() erstellte Objekte wieder zu löschen. Es gibt insbesondere keine Garbage-Collection oder ähnliches und es ist auch nicht geplant ein solches System einzuführen. Es empfiehlt sich daher das Anlegen eines Ressourcenpools, der alle dynamischen UI-Elemente verwaltet und bei Bedarf recyclet. Wahrscheinlich wird es in naher Zukunft Bibliotheken von fleißigen Entwicklern geben, die diese ganze Geschichte enorm vereinfachen, bis dahin gilt: Jeder ist für seinen Dreck selbst verantwortlich! Also räumt immer schön hinter euch auf, wir wollen ja nicht, dass jemand über euren Abfall stolpert ;)

Conclusion

Nach dieser Achterbahnfahrt quer durch den neuen Patch, hoffe ich, dass ihr einen kleinen Einblick gewinnen konntet, was sich mit 1.10 alles geändert hat. Zugegeben, einiges ist noch unausgereift und mit Sicherheit wird man Zeit brauchen um sich an die eine oder andere änderung zu gewöhnen. Im Großen und Ganzen jedoch beschert der neue Patch gerade den AddOn-Entwicklern einen Riesenhaufen netter Spielzeuge mit dem wir, und da bin ich absolut sicher, noch eine Menge Spaß haben werden.


zurück zur HowTo-übersicht
zurück zur Skript-Sektion
MoP Vorbestellung
MoP Vorschau
Umfrage

Was haltet ihr von den Cross-Realm-Zonen?

Ergebnis anzeigen »

Loading ... Loading ...
ContentAd
Klassenguides
 
... das Rennspiel "RPM Racing " vom November 1991 das erste Spiel der Blizzard-Entwickler und das erste Spiel für Nintendo SNES war, das in Amerika programmiert wurde?
Rectangle
mySigs
mySigs.de
Shakes Fidget
Facebook
Interessantes und Ungewöhnliches rund um World of Warcraft