ROOT posiada opcję pisania graficznego interfejsu. Jest to przydatne gdy chcemy "wyklikiwalnego" programu.
Zacznijmy od uproszczonej wersji ROOTowego przykładu example. Wygląda on następująco:
#include <RQ_OBJECT.h> #include <TCanvas.h> #include <TF1.h> #include <TGButton.h> #include <TGClient.h> #include <TGFrame.h> #include <TRandom.h> #include <TRootEmbeddedCanvas.h> class MyMainFrame { RQ_OBJECT("MyMainFrame") private: TGMainFrame* fMain; TRootEmbeddedCanvas* fEcanvas; public: MyMainFrame(const TGWindow* p, UInt_t w, UInt_t h); virtual ~MyMainFrame(); void DoDraw(); }; MyMainFrame::MyMainFrame(const TGWindow* p, UInt_t w, UInt_t h) { // Create a main frame fMain = new TGMainFrame(p, w, h); // Create canvas widget fEcanvas = new TRootEmbeddedCanvas("Ecanvas", fMain, 200, 200); fMain->AddFrame(fEcanvas, new TGLayoutHints(kLHintsExpandX | kLHintsExpandY, 10, 10, 10, 1)); //puste miejsce na dodanie innych obiektow fMain->SetWindowName("Simple Example"); fMain->MapSubwindows(); fMain->Resize(fMain->GetDefaultSize()); fMain->MapWindow(); } void MyMainFrame::DoDraw() { // Draws function graphics in randomly chosen interval TF1* f1 = new TF1("f1", "sin(x)/x", 0, gRandom->Rndm() * 10); f1->SetLineWidth(3); f1->Draw(); TCanvas* fCanvas = fEcanvas->GetCanvas(); fCanvas->cd(); fCanvas->Update(); } MyMainFrame::~MyMainFrame() { // Clean up used widgets: frames, buttons, layout hints fMain->Cleanup(); delete fMain; } void gui1() { // Popup the GUI... new MyMainFrame(gClient->GetRoot(), 200, 200); }
Pewną nowością jest zastosowanie makra RQ_OBJECT, jest to makro konieczne ze względu na możliwość podłączenia klasy - przynajmniej tak mówi manual bo u mnie makro działa bez takiej metody. Następnie mamy klasę MyMainFrame która symbolizuje w zasadzie cały program. I tutaj wprowadźmy dwie definicje żeby nie mylić:
- okienko - to odpowiednik TCanvas, można je utożsamić z obszarem rysowania np. histogramu
- ramka - odpowiednik okienka GUI (coś jak JFrame w Javie), odpowiada za rysowanie całego programu (łącznie z przyciskami itd.)
Do konstruktora klasy MyMainFrame podajemy gClient->GetRoot czyli wskaźnik do okienka nadrzędnego (cokolwiek to znaczy, nullptr też mi działa). Następnie mamy konstruktor, co się w nim dzieje? Najpierw tworzymy naszą ramkę czyli TGMainFrame (linia 32). Niestety wtedy zobaczymy tylko szare okno więc dodajemy GUI-TCanvas (zwykłego TCanvas nie podłączymy do GUI) czyli TRootEmbeddedCanvas w linii 36. Warto zauważyć że w linii 35 jako jeden z argumentów TRootEmbeddedCanvas podajemy wskaźnik na ramkę (fMain). Jest to charakterystyczna cecha ROOT-owego GUI - w konstruktorze obiektu podajemy wskaźnik do obiektu nadrzędnego, a potem gdy dodajemy taki obiekt przy pomocy AddFrame podajemy wskaźnik na obiekt podrzędny. Innymi słowy obiekt-matka posiada wskaźnik na córkę a obiekt-córka wskaźnik na matkę.
Następnie mamy metodę dodającą nazwę do ramki, metodę mapującą podokienka (konieczne aby widoczne były obiekty-córki, metodę resize (konieczna aby prawidłowo wypozycjonować obiekty córki - inaczej może się zdarzyć że po zmianie rozmiaru okna coś się przesunie nie tak jak trzeba), na końcu mapujemy samą ramkę (konieczne aby w ogóle coś zobaczyć.
Pozycjonowanie obiektów
W GUI ważnej jest odpowiednie ustawienie obiektów, do tego służy klasa TGLayoutHits. Pierwszy argument określa wyrównanie tutaj używać można operator | do "składania" kilku sugestii. W naszym przykładzie kLHintsExpandX | kLHintsExpandY oznacza "rozszerzaj obiekt w płaszczyźnie poziomej oraz rozszerzaj obiekt w płaszczyźnie pionowej", dzięki temu zmiana rozmiaru ramki wymusi zmianę okienka canvas. Gdybyśmy ustawili kLHintsNormal nasze okienko z wykresem nie powiększałoby się wraz z ramką. Ostatnie 4 argumenty określają w pikselach odległość między naszym okienkiem a ramką - w tym przypadku będzie to 10 pikseli od góry, prawej, lewej oraz 1 od dołu. Dlatego nasze okienko będzie wyglądało tak:
Dodawanie responsywnych obiektów
Pisząc dosyć dużą klasę doszliśmy do etapu który można zrobić w zasadzie przy pomocy TCanvas, a przecież nie o to chodzi. Chodzi o interfejs który będzie responsywny. Dodajmy więc dwa guziki Draw i Exit - pierwszy będzie rysował, a drugi zamykał program, oba będą pod okienkiem.
Niestety żeby to miało ręce i nogi trzeba się trochę "postarać" ze względu na dosyć upierdliwy mechanizm pozycjonowania. Otóż najlepiej zrobić to w sposób następujący:
- tworzymy "podramkę"
- do podramki dodajemy dwa guziki
- oprogramowywujemy dwa guziki aby rysowały wykres/zamykały program
Nasz kod musimy włożyć w miejscu oznaczonym komentarzem - gwarantuje to że obiekty będą narysowane od razu jak pojawi się okienko. Kod który należy dodać wygląda następująco:
TGHorizontalFrame* hframe = new TGHorizontalFrame(fMain, 200, 40); hframe->SetBackgroundColor(12); TGTextButton* draw = new TGTextButton(hframe, "&Draw"); draw->Connect("Clicked()", "MyMainFrame", this, "DoDraw()"); hframe->AddFrame(draw, new TGLayoutHints(kLHintsCenterX, 5, 5, 3, 4)); TGTextButton* quitb = new TGTextButton(hframe, "&Exit", "gApplication->Terminate(0)"); hframe->AddFrame(quitb, new TGLayoutHints(kLHintsCenterX, 5, 5, 3, 4)); fMain->AddFrame(hframe, new TGLayoutHints(kLHintsCenterX, 2, 2, 2, 2));
Jako "podramki" użyjemy obiektu TGHorizontalFrame czyli ramki która układy dzieci poziomo (tj. jedno obok drugiego). Tutaj lekko zmodyfikowałem przykład ROOTowy - dodając metodę ustawiającą kolor tła tej ramki (po to abyście zobaczyli gdzie fizycznie ten obiekt jest). Sama ramka jest tworzona w pierwszej linii powyższego kodu a dodawana w ostatniej.
W linii 4-6 dodajemy guzik rysowania. Wymaga to co najmniej dwóch linii kodu - konstruktora który tworzy obiekt oraz AddFrame która dodaje guzik do obiektu-matki - w tym wypadku "podramki". Taki guzik będzie widoczny ale nie będzie nic robił dlatego konieczne jest podpięcie pod niego jakieś funkcji, robi się to metodą Connect, jako argument metoda ta przyjmuje: sygnał wejściowy (guzik kliknięto) nazwę "klasy otrzymującej sygnał", wskaźnik na tę klasę ostatnim argumentem jest tzw. "slot" czyli nazwa metody która jest wywoływana. My wywołujemy metodę MyMainFrame::DoDraw(). Jak widać taka architektura pozwala przy pomocy sygnału wywoływać określoną metodę na dowolnym obiekcie dowolnej klasy.
Guzi wyjścia działą nieco inaczej - zamiast używać metody Connect funkcja jest dodawana bezpośrednio w konstruktorze - jest to argument "gApplication->Terminate(0)" czyli zakończenie bieżącego programu.
Jeszcze inne obiekty gdy są aktywowane wysyłają coś więcej jak "sygnał kliknięcia" np. TGButtonGroup - jest używany często jako lista wyboru (w formie tzw. radio buttonów), gdy jeden z radio buttonów zostaje wybrany wysyłane jest nie "Pressed()" ale "Pressed(Int_t)" gdzie argumentem funkcji jest ID wybranego radio buttona, w ty przypadku można taką informację przekazać np. wywołując "DoDraw2(Int_t)", oczywiście nasza klasa musi wtedy posiadać metodę DoDraw2(Int_t).
Wybrame widżety i wybrane metody dostępu do danych
Poniżej minimalne przykłady zastosowania pewnych widżetów. Jako podstawę użyję klasy RootFrame
#include <RQ_OBJECT.h> #include <TCanvas.h> #include <TF1.h> #include <TGButton.h> #include <TGClient.h> #include <TGFrame.h> #include <TRandom.h> #include <TRootEmbeddedCanvas.h> class RootFrame : public TGMainFrame { RQ_OBJECT("RootFrame") public: RootFrame(const TGWindow* p, UInt_t w, UInt_t h); virtual ~RootFrame(); void CallInt(Int_t i) { std::cout < "called " << i << std::endl; } void CallNull() { std::cout < "called null" << std::endl; }
void DoSomething(){}; void AddWidget(); }; RootFrame::RootFrame(const TGWindow* p, UInt_t w, UInt_t h) : TGMainFrame(p, w, h) { AddWidget(); SetWindowName("Simple Example"); MapSubwindows(); Resize(GetDefaultSize()); MapWindow(); } void RootFrame::AddWidget() { TGTextButton* draw = new TGTextButton(this, "&CallNull"); draw->Connect("Clicked()", "RootFrame", this, "CallNull()"); AddFrame(draw, new TGLayoutHints(kLHintsCenterX, 5, 5, 3, 4)); } RootFrame::~RootFrame() { Cleanup(); } void gui3() { // Popup the GUI... new RootFrame(nullptr, 200, 200); }
Przy omawianiu widgetów będę nadpisywał metodę AddWidget, ewentualnie DoSomething. Ponieważ zaczęliśmy od guzików tutaj zacząłem od przykładowego użycai TGTextButton który po nacisnięciu przycisku wyprintuje "Called null".
Radio buttony
Radio buttony używane są do wybierania spośród jednej z kilku opcji. Aby zobaczyć jak działają nadpisujemy AddWidget:
void RootFrame::AddWidget() { TGButtonGroup* br = new TGButtonGroup(this, "Some val", kVerticalFrame); TGRadioButton** R = new TGRadioButton*[3]; R[0] = new TGRadioButton(br, new TGHotString("&X")); R[1] = new TGRadioButton(br, new TGHotString("&Y ")); R[2] = new TGRadioButton(br, new TGHotString("&Z ")); R[1]->SetState(kButtonDown); br->Connect("Pressed(Int_t)", "RootFrame", this, "CallInt(Int_t)"); // br->Show(); AddFrame(br, new TGLayoutHints(kLHintsCenterX, 5, 5, 3, 4)); }
Tworzymy tutaj TGButtonGroup które trzyma nam kolekcję TGRadioButton, w tym kodzie chcemy aby po wybraniu któregoś radio-buttona (Pressed) wyświetlno jego ID (dlatego łączymy Pressed(Int_t) z CallInt(Int_t), gdy wciśniemy X zostanie wyświetlone "called 1", gdy wybierzemy Y "called 2" zaś gdy wybierzemy Z "called 3".
Check buttony
Kolejny rodzaj guzików, tym razem używany do zaznaczania/odznaczania opcji.
void RootFrame::AddWidget() { TGCheckButton* estat = new TGCheckButton(this, "Check button", 1); estat->SetState(kButtonDown); estat->Connect("Toggled(Bool_t)", "RootFrame", this, "CallNull()"); AddFrame(estat, new TGLayoutHints(kLHintsCenterX, 5, 5, 3, 4)); }
Number entries
Jest to pole do wpisywania wartości numerycznych. Ponieważ często jest tak że chcemy się do takiej wartości dobrać (samo obsłużenie kliknięcia nie wystarczy) dodałem TGNumberEntry do klasy RootFrame (jako pole). Stworzyłem też dodatkową metodę CallMagic która pokazuje jak użycać wartości z takiego pola. Tutaj ważna uwaga w przykładzie pole służy do pobierania wartości całkowitych ale jest bardzo konfigurowalne - może służyć do obsługi liczby zmiennoprzecinkowych albo jedynie dodatnich, w tym przypadku służy do liczb całkowitych dlatego w konstruktorze mamy TGNumberFormat::kNESInteger.
Samo pole number entry nie posiada żadnego opisu jak wcześniej wymienione widżety, dlatego należało dodać TGLabel. Aby TGLabel było wyświetlane obok TGNumberEntry na dokładnie tej samej wysokości itd. wykorzystałem TGHorizontalFrame.
void RootFrame::AddWidget() { TGHorizontalFrame* frame = new TGHorizontalFrame(this); fEntry = new TGNumberEntry(frame, 0, 6, -1, TGNumberFormat::kNESInteger); fEntry->Connect("ValueSet(Long_t)", "RootFrame", this, "CallMagic()"); frame->AddFrame(fEntry, new TGLayoutHints(kLHintsLeft, 5, 5, 5, 5)); TGLabel* label = new TGLabel(frame, "Some label"); frame->AddFrame(label, new TGLayoutHints(kLHintsCenterY, 5, 5, 5, 5)); AddFrame(frame, new TGLayoutHints(kLHintsCenterY, 5, 5, 5, 5)); } void RootFrame::CallMagic() { std::cout << fEntry->GetIntNumber() << std::endl; }
Inne obiekty
Wymienione tutaj obiekty to tylko wierzchołek możliwości ROOTowego GUI, dlatego zachęcam do lektury rozdziału writing gui. Polecam się zapoznać również z TGuiBuilder jest to klasa do "wyklikiwania" sobie GUI. TGuiBuidler można wywołać z ROOTa wpisując po prostu new TGuiBuilder .