English Русский 中文 Español 日本語 Português
preview
DoEasy. Steuerung (Teil 16): TabControl WinForms-Objekt — mehrere Reihen von Registerkarten-Kopfzeilen, Dehnung der Kopfzeilen zur Anpassung an den Container

DoEasy. Steuerung (Teil 16): TabControl WinForms-Objekt — mehrere Reihen von Registerkarten-Kopfzeilen, Dehnung der Kopfzeilen zur Anpassung an den Container

MetaTrader 5Beispiele | 3 November 2022, 12:02
105 0
Artyom Trishkin
Artyom Trishkin

Inhalt


Konzept

Im vorangegangenen Artikel habe ich die Modi für die Anzeige von Registerkartenüberschriften erläutert:

Wenn ein Steuerelement mehr Registerkarten hat, als in die Objektbreite passen (wir gehen davon aus, dass sie sich oben befinden), dann können die Titel, die nicht in das Element passen, abgeschnitten werden und die Bildlaufschaltflächen erhalten. Wenn für das Objekt das Kennzeichen für den Mehrzeilenmodus gesetzt ist, werden die Titel alternativ in mehreren Teilen (je nachdem, wie viele in der Größe des Elements enthalten sind) und in mehreren Zeilen platziert. Es gibt drei Möglichkeiten, die Größe der in Reihen angeordneten Registerkarten festzulegen(SizeMode):

  • Normal — die Breite der Tabs wird entsprechend der Breite des Titeltextes festgelegt, der in den Werten PaddingWidth und PaddingHeight des Titels angegebene Platz wird entlang der Kanten des Titels hinzugefügt;
  • Fixed — feste Größe, die in den Steuerungseinstellungen angegeben ist. Der Titeltext wird abgeschnitten, wenn er nicht in seine Größe passt;
  • FillToRight — Registerkarten, die in die Breite eines Steuerelements passen, werden zur vollen Breite aufgefüllt.

Wenn eine Registerkarte im aktiven mehrzeiligen Modus ausgewählt wird, wird ihr Titel, der nicht an das Registerkartenfeld angrenzt, zusammen mit der gesamten Zeile, in der sie sich befindet, in die Nähe des Registerkartenfelds verschoben, und die Titel, die an das Feld angrenzten, nehmen den Platz der Zeile der ausgewählten Registerkarte ein.


Ich habe auch die Funktionalität der Platzierung von Registerkarten auf der Oberseite des Steuerelements in den Modi Normal und Feststehend implementiert.

In diesem Artikel werde ich Registerkarten im Mehrzeilenmodus auf allen Seiten des Steuerelements platzieren und den Modus zum Einstellen der Größe der Registerkarten FillToRight hinzufügen — die Reihen der Registerkarten entsprechend der Größe des Steuerelements strecken. Wenn Reihen von Registerkartenüberschriften am oberen oder unteren Rand des Containers platziert werden, werden die Überschriften auf die Breite des Steuerelements gedehnt. Wenn die Kopfzeilen der Registerkarten nach links oder rechts verschoben werden, werden die Kopfzeilen an die Höhe des Steuerelements angepasst. In diesem Fall wird der Kopfzeilen-Streckbereich auf jeder Seite dieses Bereichs um zwei Pixel kleiner, da die ausgewählte Registerkarte beim Anklicken um 4 Pixel größer wird. Wenn wir also keine Lücke von zwei Pixeln für die marginale Überschrift lassen, dann geht ihr Rand über die Kontrolle hinaus, wenn sie ausgewählt wird und ihre Größe entsprechend zunimmt.

Für den Modus der Anzeige von Registerkartenüberschriften in einer Zeile und wenn es mehr von ihnen gibt, als durch die Größe des Steuerelements passen, werden alle Überschriften, die über den Container hinausgehen, außerhalb des Containers angeordnet. Wir verfügen noch nicht über ausreichende Funktionen, um grafische Elemente, die über den Container hinausgehen, zu beschneiden, daher wird dieser Modus noch nicht berücksichtigt. Ich werde sie in späteren Artikeln umsetzen.


Verbesserung der Bibliotheksklassen

Wir müssen in den Listen der Registerkartenüberschriften nach allen Überschriften suchen, die sich in derselben Zeile befinden, damit wir nur mit den Überschriften dieser Zeile arbeiten können. Am einfachsten ist es, dem WinForms-Objekt der Bibliothek neue Eigenschaften hinzuzufügen und die seit langem bestehenden Funktionen der Bibliothek zum Suchen und Sortieren von Objektlisten zu nutzen.

In \MQL5\Include\DoEasy\Defines.mqh fügen wir der Liste der Ganzzahl-Eigenschaften des grafischen Elements auf der Leinwand zwei neue Eigenschaften hinzu und erhöhen deren Gesamtzahl von 90 auf 92:

//+------------------------------------------------------------------+
//| Integer properties of the graphical element on the canvas        |
//+------------------------------------------------------------------+
enum ENUM_CANV_ELEMENT_PROP_INTEGER
  {
   CANV_ELEMENT_PROP_ID = 0,                          // Element ID
   CANV_ELEMENT_PROP_TYPE,                            // Graphical element type

   //---...
   //---...

   CANV_ELEMENT_PROP_TAB_SIZE_MODE,                   // Tab size setting mode
   CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,                 // Tab index number
   CANV_ELEMENT_PROP_TAB_PAGE_ROW,                    // Tab row index
   CANV_ELEMENT_PROP_TAB_PAGE_COLUMN,                 // Tab column index
   CANV_ELEMENT_PROP_ALIGNMENT,                       // Location of an object inside the control
   
  };
#define CANV_ELEMENT_PROP_INTEGER_TOTAL (92)          // Total number of integer properties
#define CANV_ELEMENT_PROP_INTEGER_SKIP  (0)           // Number of integer properties not used in sorting
//+------------------------------------------------------------------+


Wir sortieren nach neuer Eigenschaft zur Enumeration möglicher Kriterien zum Sortieren von grafischen Elementen auf der Leinwand hinzufügen:

//+------------------------------------------------------------------+
//| Possible sorting criteria of graphical elements on the canvas    |
//+------------------------------------------------------------------+
#define FIRST_CANV_ELEMENT_DBL_PROP  (CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_INTEGER_SKIP)
#define FIRST_CANV_ELEMENT_STR_PROP  (CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_INTEGER_SKIP+CANV_ELEMENT_PROP_DOUBLE_TOTAL-CANV_ELEMENT_PROP_DOUBLE_SKIP)
enum ENUM_SORT_CANV_ELEMENT_MODE
  {
//--- Sort by integer properties
   SORT_BY_CANV_ELEMENT_ID = 0,                       // Sort by element ID
   SORT_BY_CANV_ELEMENT_TYPE,                         // Sort by graphical element type

   //---...
   //---...

   SORT_BY_CANV_ELEMENT_TAB_SIZE_MODE,                // Sort by the mode of setting the tab size
   SORT_BY_CANV_ELEMENT_TAB_PAGE_NUMBER,              // Sort by the tab index number
   SORT_BY_CANV_ELEMENT_TAB_PAGE_ROW,                 // Sort by tab row index
   SORT_BY_CANV_ELEMENT_TAB_PAGE_COLUMN,              // Sort by tab column index
   SORT_BY_CANV_ELEMENT_ALIGNMENT,                    // Sort by the location of the object inside the control
//--- Sort by real properties

//--- Sort by string properties
   SORT_BY_CANV_ELEMENT_NAME_OBJ = FIRST_CANV_ELEMENT_STR_PROP,// Sort by an element object name
   SORT_BY_CANV_ELEMENT_NAME_RES,                     // Sort by the graphical resource name
   SORT_BY_CANV_ELEMENT_TEXT,                         // Sort by graphical element text
   SORT_BY_CANV_ELEMENT_DESCRIPTION,                  // Sort by graphical element description
  };
//+------------------------------------------------------------------+

Jetzt können wir schnell alle Registerkartenüberschriften, die sich in derselben Zeile befinden, finden und zur Liste hinzufügen und ihre Gesamtgröße so berechnen, dass sie sich auf die Größe des Steuerelements ausdehnen und korrekt platziert werden.


Wenn wir dem Objekt neue Eigenschaften hinzufügen, müssen wir auch deren Beschreibung hinzufügen. Alle Eigenschaftsbeschreibungen befinden sich in einem mehrdimensionalen Array, wobei die erste Dimension den Nachrichtenindex enthält und die übrigen Dimensionen Texte in verschiedenen Sprachen enthalten. Im Moment haben wir Nachrichtentexte in Englisch und Russisch, aber andere Sprachen können leicht hinzugefügt werden, indem man die Array-Dimension vergrößert und Texte in den entsprechenden Sprachen in den erforderlichen Dimensionen hinzufügt.

In \MQL5\Include\DoEasy\Data.mqh wurden neuen Nachrichtenindizes hinzugefügt:

   MSG_CANV_ELEMENT_PROP_TAB_SIZE_MODE,               // Tab size setting mode
   MSG_CANV_ELEMENT_PROP_TAB_PAGE_NUMBER,             // Tab index number
   MSG_CANV_ELEMENT_PROP_TAB_PAGE_ROW,                // Tab row index
   MSG_CANV_ELEMENT_PROP_TAB_PAGE_COLUMN,             // Tab column index
   MSG_CANV_ELEMENT_PROP_ALIGNMENT,                   // Location of an object inside the control
//--- Real properties of graphical elements

//--- String properties of graphical elements
   MSG_CANV_ELEMENT_PROP_NAME_OBJ,                    // Graphical element object name
   MSG_CANV_ELEMENT_PROP_NAME_RES,                    // Graphical resource name
   MSG_CANV_ELEMENT_PROP_TEXT,                        // Graphical element text
   MSG_CANV_ELEMENT_PROP_DESCRIPTION,                 // Graphical element description
  };
//+------------------------------------------------------------------+

und die Textnachrichten, die den neu hinzugefügten Indizes entsprechen:

   {"Режим установки размера вкладок","Tab Size Mode"},
   {"Порядковый номер вкладки","Tab ordinal number"},
   {"Номер ряда вкладки","Tab row number"},
   {"Номер столбца вкладки","Tab column number"},
   {"Местоположение объекта внутри элемента управления","Location of the object inside the control"},
   
//--- String properties of graphical elements
   {"Имя объекта-графического элемента","The name of the graphic element object"},
   {"Имя графического ресурса","Image resource name"},
   {"Текст графического элемента","Text of the graphic element"},
   {"Описание графического элемента","Description of the graphic element"},
  };
//+---------------------------------------------------------------------+

Ich habe die Nachrichtenklasse der Bibliothek und das Konzept der Speicherung ihrer Daten in einem separaten Artikel behandelt.


Bei der Verwendung der Klasse CCanvas aus der MQL5-Standardbibliothek ist es nicht immer möglich, den Code des Fehlers zu erhalten, der uns daran hinderte, ein grafisches Element zu erstellen. Ich füge nach und nach Korrekturen in die Bibliothek ein, damit die Nutzer verstehen, warum ein Element nicht erstellt wurde.

Wir fügen in der Datei \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh für grafische Elemente, und zwar in den Methoden zum Festlegen der Breite und Höhe, eine Beschreibung des Größenänderungsfehlers zusammen mit der Anzeige der Fehlermeldung hinzu:

//+------------------------------------------------------------------+
//| Set a new width                                                  |
//+------------------------------------------------------------------+
bool CGCnvElement::SetWidth(const int width)
  {
   if(this.GetProperty(CANV_ELEMENT_PROP_WIDTH)==width)
      return true;
   if(!this.m_canvas.Resize(width,this.m_canvas.Height()))
     {
      CMessage::ToLog(DFUN+this.TypeElementDescription()+": width="+(string)width+": ",MSG_CANV_ELEMENT_ERR_FAILED_SET_WIDTH);
      return false;
     }
   this.SetProperty(CANV_ELEMENT_PROP_WIDTH,width);
   return true;
  }
//+------------------------------------------------------------------+
//| Set a new height                                                 |
//+------------------------------------------------------------------+
bool CGCnvElement::SetHeight(const int height)
  {
   if(this.GetProperty(CANV_ELEMENT_PROP_HEIGHT)==height)
      return true;
   if(!this.m_canvas.Resize(this.m_canvas.Width(),height))
     {
      CMessage::ToLog(DFUN+this.TypeElementDescription()+": height="+(string)height+": ",MSG_CANV_ELEMENT_ERR_FAILED_SET_HEIGHT);
      return false;
     }
   this.SetProperty(CANV_ELEMENT_PROP_HEIGHT,height);
   return true;
  }
//+------------------------------------------------------------------+

Hier erhält die Makrosubstitution eine Beschreibung des Elementtyps, dessen Größe nicht geändert werden konnte, und den an die Methode übergebenen Parameterwert. Das macht es einfacher, Fehler bei der Entwicklung von Bibliotheksklassen zu verstehen.


Um die Beschreibungen der beiden neuen Eigenschaften eines grafischen Elements anzeigen zu können, müssen wir der Methode, die die Beschreibung der Ganzzahl-Eigenschaft des Elements zurückgibt, in \MQL5\Include\DoEasy\Objects\Graph\WForms\WinFormBase.mqh einen Codeblock hinzufügen:

//+------------------------------------------------------------------+
//| Return the description of the control integer property           |
//+------------------------------------------------------------------+
string CWinFormBase::GetPropertyDescription(ENUM_CANV_ELEMENT_PROP_INTEGER property,bool only_prop=false)
  {
   return
     (
      property==CANV_ELEMENT_PROP_ID                           ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_ID)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==CANV_ELEMENT_PROP_TYPE                         ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_TYPE)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+this.TypeElementDescription()
         )  :
      
      //---...
      //---...

      property==CANV_ELEMENT_PROP_TAB_SIZE_MODE                ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_SIZE_MODE)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+TabSizeModeDescription((ENUM_CANV_ELEMENT_TAB_SIZE_MODE)this.GetProperty(property))
         )  :
      property==CANV_ELEMENT_PROP_TAB_PAGE_NUMBER              ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_PAGE_NUMBER)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==CANV_ELEMENT_PROP_TAB_PAGE_ROW                 ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_PAGE_ROW)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==CANV_ELEMENT_PROP_TAB_PAGE_COLUMN              ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_TAB_PAGE_COLUMN)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+(string)this.GetProperty(property)
         )  :
      property==CANV_ELEMENT_PROP_ALIGNMENT                    ?  CMessage::Text(MSG_CANV_ELEMENT_PROP_ALIGNMENT)+
         (only_prop ? "" : !this.SupportProperty(property)     ?  ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
          ": "+AlignmentDescription((ENUM_CANV_ELEMENT_ALIGNMENT)this.GetProperty(property))
         )  :
      ""
     );
  }
//+------------------------------------------------------------------+

Hier erstellen wir je nach der an die Methode übergebenen Eigenschaft eine Textnachricht und lassen diese von der Methode zurückgeben.


Lassen Sie uns nun die Klassen des TabControl WinForms Objekts finalisieren.

Das Objekt besteht aus einem Container, in dem sich die Registerkarten befinden. Der Container wiederum besteht aus zwei Hilfsobjekten — dem Registerfeld und seiner Überschrift. Alle Objekte, die sich auf der Registerkarte befinden sollen, werden auf dem Registerkartenfeld platziert, und wir wählen die Registerkarte, die wir sehen und mit der wir arbeiten wollen, als Registerkartenkopf aus. Registerkartentitel sind in der Klasse TabHeader implementiert, die von dem WinForms-Objekt Button in \MQL5\Include\DoEasy\Objects\Graph\WForms\TabHeader.mqh abgeleitet ist.

Da wir jetzt zwei neue Eigenschaften haben, die die Position der Registerkarte Kopfzeile in der allgemeinen Kopfzeilenliste angeben (Kopfzeile und Spalte, die verwendet werden, um sie auf einem Steuerelement zu platzieren), entfernen wir zwei private Variablen, die diese Eigenschaften gespeichert haben:

//+------------------------------------------------------------------+
//| TabHeader object class of WForms TabControl                      |
//+------------------------------------------------------------------+
class CTabHeader : public CButton
  {
private:
   int               m_width_off;                        // Object width in the released state
   int               m_height_off;                       // Object height in the released state
   int               m_width_on;                         // Object width in the selected state
   int               m_height_on;                        // Object height in the selected state
   int               m_col;                              // Header column index
   int               m_row;                              // Header row index
//--- Adjust the size and location of the element depending on the state
   bool              WHProcessStateOn(void);
   bool              WHProcessStateOff(void);
//--- Draws a header frame depending on its position
   virtual void      DrawFrame(void);
//--- Set the string of the selected tab header to the correct position, (1) top, (2) bottom, (3) left and (4) right
   void              CorrectSelectedRowTop(void);
   void              CorrectSelectedRowBottom(void);
   void              CorrectSelectedRowLeft(void);
   void              CorrectSelectedRowRight(void);
   
protected:


In den öffentlichen Methoden, die diese beiden Eigenschaften setzen und zurückgeben, werden ihre Werte nun auf die Objekteigenschaften gesetzt (und von diesen zurückgegeben) und nicht mehr auf die Variablen, wie es vorher der Fall war:

//--- Returns the control size
   int               WidthOff(void)                                                    const { return this.m_width_off;          }
   int               HeightOff(void)                                                   const { return this.m_height_off;         }
   int               WidthOn(void)                                                     const { return this.m_width_on;           }
   int               HeightOn(void)                                                    const { return this.m_height_on;          }
//--- (1) Set and (2) return the Tab row index
   void              SetRow(const int value)                { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_ROW,value);            }
   int               Row(void)                        const { return (int)this.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_ROW);      }
//--- (1) Set and (2) return the Tab column index
   void              SetColumn(const int value)             { this.SetProperty(CANV_ELEMENT_PROP_TAB_PAGE_COLUMN,value);         }
   int               Column(void)                     const { return (int)this.GetProperty(CANV_ELEMENT_PROP_TAB_PAGE_COLUMN);   }
//--- Set the tab location
   void              SetTabLocation(const int row,const int col)
                       {
                        this.SetRow(row);
                        this.SetColumn(col);
                       }


Das Objekt für die Registerkartenüberschrift wird im Konstruktor mit Standardwerten für die Größe erstellt und dann an den Größenmodus für die Überschrift angepasst, der in der Objektcontainerklasse festgelegt wurde. Dies liegt daran, dass wir für alle WinForms-Objekte der Bibliothek die gleichen Werte ihrer Parameter verwenden, die allen Objekten gemeinsam sind, während wir zusätzliche Parameter, die zu einem bestimmten Objekt gehören, nach dessen Erstellung einstellen. Dies hat sowohl Vorteile als auch Einschränkungen. Die Bequemlichkeit liegt in der Tatsache, dass wir jedes Objekt mit einer Methode erstellen können, während die Einschränkungen darin liegen, dass es nicht immer möglich ist, ein Objekt sofort mit den gewünschten Werten seiner Eigenschaften zu erstellen, und diese nach der Erstellung des Objekts installiert werden müssen.

In diesem Fall handelt es sich um die Kopfgröße, die vom Größeneinstellungsmodus abhängt. In diesem Fall ändert sich die Größe des ursprünglich in den angegebenen Koordinaten erstellten Objekts weiter, und seine Anfangskoordinate, die sich in der oberen linken Ecke des konstruierten Objekts befindet, ist nicht mehr dort, wo sie ursprünglich geplant war. Daher müssen wir nicht nur die Größe der Kopfzeile ändern, sondern auch ihre Position in der gewünschten Koordinate kontrollieren, da das Objekt in einigen Fällen verschoben wird und sich außerhalb seines Containers befindet.

Für den Modus „Normal“ können wir im Voraus herausfinden, welche Größen die Kopfzeile erhalten wird. In diesem Modus passt sich die Größe des Objekts an den darauf geschriebenen Text an, und dieser Text ist uns bekannt. Wenn sich Kopfzeilen am oberen und unteren Rand des Containers befinden, werden die für das Objekt festgelegten Auffüllungswerte (PaddingLeft und PaddingRight) zu der durch die Textgröße des Objekts berechneten Breite und Höhe addiert, während PaddingTop und PaddingBottom zur Höhe addiert werden. Wenn Kopfzeilen links und rechts vom Container (vertikal) positioniert sind, werden PaddingLeft und PaddingRight zur Höhe des Objekts und PaddingTop und PaddingBottom zur Breite hinzugefügt, da es optisch so aussieht, als sei die Kopfzeile einfach um 90° gedreht und ihr Text vertikal, sodass die tatsächliche Höhe des grafischen Elements die sichtbare Breite des vertikal gedrehten Objekts ist.

Nehmen wir Änderungen an der Methode vor, die alle Titelgrößen festlegt. Im Codeblock für die Einstellung der Kopfzeilengröße auf die Textgröße für den Modus „Normal“ wird die Steuerung der Seite des Containers, auf der sich die Kopfzeilen befinden, implementiertum die Kopfzeilen oben und unten einzustellen, werden die Padding-Werte in der richtigen Reihenfolge hinzugefügt — Padding wird zur Breite links und rechts hinzugefügt, während es im Falle der Höhe oben und unten hinzugefügt wird. Um Kopfzeilen links und rechts zu platzieren, werden die zu Breite und Höhe hinzugefügten Auffüllungswerte vertauscht — die Auffüllung oben und unten wird zur Breite und die Auffüllung links und rechts wird zur Höhe hinzugefügt:

//--- Depending on the header size setting mode
   switch(this.TabSizeMode())
     {
      //--- set the width and height for the Normal mode
      case CANV_ELEMENT_TAB_SIZE_MODE_NORMAL :
        switch(this.Alignment())
          {
           case CANV_ELEMENT_ALIGNMENT_TOP      :
           case CANV_ELEMENT_ALIGNMENT_BOTTOM   :
              this.TextSize(this.Text(),width,height);
              width+=this.PaddingLeft()+this.PaddingRight();
              height=h+this.PaddingTop()+this.PaddingBottom();
             break;
           case CANV_ELEMENT_ALIGNMENT_LEFT     :
           case CANV_ELEMENT_ALIGNMENT_RIGHT    :
              this.TextSize(this.Text(),height,width);
              height+=this.PaddingLeft()+this.PaddingRight();
              width=w+this.PaddingTop()+this.PaddingBottom();
             break;
           default:
             break;
          }
        break;
      //---CANV_ELEMENT_TAB_SIZE_MODE_FIXED
      //---CANV_ELEMENT_TAB_SIZE_MODE_FILL
      //--- For the Fixed mode, the dimensions remain specified,
      //--- In case of Fill, they are calculated in the StretchHeaders methods of the TabControl class
      default: break;
     }
//--- Set the results of changing the width and height to 'res'


Am Ende der Methode werden die Größen für die Kopfzeile im markierten und nicht markierten Zustand auf genau dieselbe Weise festgelegt. Da bei der Auswahl einer Registerkarte durch einen Klick auf die Kopfzeile die Kopfzeile der ausgewählten Registerkarte sich an drei Seiten um zwei Pixel vergrößert, sollte die tatsächliche Größe der horizontal angeordneten Kopfzeile in der Breite um 4 Pixel und in der Höhe um zwei Pixel erhöht werden. Wenn eine Kopfzeile vertikal angeordnet ist, entspricht die tatsächliche Objektbreite der Höhe der vertikal gedrehten Kopfzeile und die tatsächliche Höhe der Breite der Kopfzeile. In diesem Fall wird die Breite um zwei und die Höhe um vier Pixel erhöht:

//--- Set the changed size for different button states
   switch(this.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
         this.SetWidthOn(this.Width()+4);
         this.SetHeightOn(this.Height()+2);
         this.SetWidthOff(this.Width());
         this.SetHeightOff(this.Height());
        break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
         this.SetWidthOn(this.Width()+2);
         this.SetHeightOn(this.Height()+4);
         this.SetWidthOff(this.Width());
         this.SetHeightOff(this.Height());
        break;
      default:
        break;
     }


Jetzt sieht die Methode mit allen implementierten Änderungen wie folgt aus:

//+------------------------------------------------------------------+
//| Set all header sizes                                             |
//+------------------------------------------------------------------+
bool CTabHeader::SetSizes(const int w,const int h)
  {
//--- If the passed width or height is less than 4 pixels, 
//--- make them equal to four pixels
   int width=(w<4 ? 4 : w);
   int height=(h<4 ? 4 : h);
//--- Depending on the header size setting mode
   switch(this.TabSizeMode())
     {
      //--- set the width and height for the Normal mode
      case CANV_ELEMENT_TAB_SIZE_MODE_NORMAL :
        switch(this.Alignment())
          {
           case CANV_ELEMENT_ALIGNMENT_TOP      :
           case CANV_ELEMENT_ALIGNMENT_BOTTOM   :
              this.TextSize(this.Text(),width,height);
              width+=this.PaddingLeft()+this.PaddingRight();
              height=h+this.PaddingTop()+this.PaddingBottom();
             break;
           case CANV_ELEMENT_ALIGNMENT_LEFT     :
           case CANV_ELEMENT_ALIGNMENT_RIGHT    :
              this.TextSize(this.Text(),height,width);
              height+=this.PaddingLeft()+this.PaddingRight();
              width=w+this.PaddingTop()+this.PaddingBottom();
             break;
           default:
             break;
          }
        break;
      //---CANV_ELEMENT_TAB_SIZE_MODE_FIXED
      //---CANV_ELEMENT_TAB_SIZE_MODE_FILL
      //--- For the Fixed mode, the dimensions remain specified,
      //--- In case of Fill, they are calculated in the StretchHeaders methods of the TabControl class
      default: break;
     }
//--- Set the results of changing the width and height to 'res'
   bool res=true;
   res &=this.SetWidth(width);
   res &=this.SetHeight(height);
//--- If there is an error in changing the width or height, return 'false'
   if(!res)
      return false;
//--- Set the changed size for different button states
   switch(this.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
         this.SetWidthOn(this.Width()+4);
         this.SetHeightOn(this.Height()+2);
         this.SetWidthOff(this.Width());
         this.SetHeightOff(this.Height());
        break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
         this.SetWidthOn(this.Width()+2);
         this.SetHeightOn(this.Height()+4);
         this.SetWidthOff(this.Width());
         this.SetHeightOff(this.Height());
        break;
      default:
        break;
     }
   return true;
  }
//+------------------------------------------------------------------+


In der Methode, die die Größe und Position des Elements im „ausgewählten“ Zustand in Abhängigkeit von seiner Position anpasst, habe ich bisher keine Verschiebung der vergrößerten Kopfzeile für Fälle implementiert, in denen sich die Titel links und rechts des Steuerelements befinden. Außerdem habe ich einen kleinen Fehler im Verarbeitungsblock für die untere Kopfzeile gemacht — ich habe den „break“-Operator übersprungen, was keine Fehler verursacht hat, da alle Fälle leer waren und kein Code aufgerufen wurde. Dies führt nun zu einem falschen Verhalten — der Fall, der auf die übersprungene „break“-Anweisung folgt, wird verarbeitet.

Fügen wir die Codeblöcke hinzu, die die vergrößerte Kopfzeile um zwei Punkte in die richtige Richtung verschieben, um Kopfzeilen nach links und rechts zu positionieren:

//+------------------------------------------------------------------+
//| Adjust the element size and location                             |
//| in the "selected" state depending on its location                |
//+------------------------------------------------------------------+
bool CTabHeader::WHProcessStateOn(void)
  {
//--- If failed to set a new size, leave
   if(!this.SetSizeOn())
      return false;
//--- Get the base object
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return false;
//--- Depending on the title location,
   switch(this.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
        //--- Adjust the location of the row with the selected header
        this.CorrectSelectedRowTop();
        //--- shift the header by two pixels to the new location coordinates and
        //--- set the new relative coordinates
        if(this.Move(this.CoordX()-2,this.CoordY()-2))
          {
           this.SetCoordXRelative(this.CoordXRelative()-2);
           this.SetCoordYRelative(this.CoordYRelative()-2);
          }
        break;
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
        //--- Adjust the location of the row with the selected header
        this.CorrectSelectedRowBottom();
        //--- shift the header by two pixels to the new location coordinates and
        //--- set the new relative coordinates
        if(this.Move(this.CoordX()-2,this.CoordY()))
          {
           this.SetCoordXRelative(this.CoordXRelative()-2);
           this.SetCoordYRelative(this.CoordYRelative());
          }
        break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
        //--- Adjust the location of the row with the selected header
        this.CorrectSelectedRowLeft();
        //--- shift the header by two pixels to the new location coordinates and
        //--- set the new relative coordinates
        if(this.Move(this.CoordX()-2,this.CoordY()-2))
          {
           this.SetCoordXRelative(this.CoordXRelative()-2);
           this.SetCoordYRelative(this.CoordYRelative()-2);
          }
        break;
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
        //--- Adjust the location of the row with the selected header
        this.CorrectSelectedRowRight();
        //--- shift the header by two pixels to the new location coordinates and
        //--- set the new relative coordinates
        if(this.Move(this.CoordX(),this.CoordY()-2))
          {
           this.SetCoordXRelative(this.CoordXRelative());
           this.SetCoordYRelative(this.CoordYRelative()-2);
          }
        break;
      default:
        break;
     }
   return true;
  }
//+------------------------------------------------------------------+


Fügen wir die Codeblöcke hinzu, die die Kopfzeile mit der wiederhergestellten Größe wieder an ihre ursprüngliche Position verschieben, nachdem die Kopfzeile bei der Vergrößerung in der obigen Methode während ihrer Auswahl verschoben wurde:

//+------------------------------------------------------------------+
//| Adjust the element size and location                             |
//| in the "released" state depending on its location                |
//+------------------------------------------------------------------+
bool CTabHeader::WHProcessStateOff(void)
  {
//--- If failed to set a new size, leave
   if(!this.SetSizeOff())
      return false;
//--- Depending on the title location,
   switch(this.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
        //--- shift the header to its original position and set the previous relative coordinates
        if(this.Move(this.CoordX()+2,this.CoordY()+2))
          {
           this.SetCoordXRelative(this.CoordXRelative()+2);
           this.SetCoordYRelative(this.CoordYRelative()+2);
          }
        break;
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
        //--- shift the header to its original position and set the previous relative coordinates
        if(this.Move(this.CoordX()+2,this.CoordY()))
          {
           this.SetCoordXRelative(this.CoordXRelative()+2);
           this.SetCoordYRelative(this.CoordYRelative());
          }
        break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
        //--- shift the header to its original position and set the previous relative coordinates
        if(this.Move(this.CoordX()+2,this.CoordY()+2))
          {
           this.SetCoordXRelative(this.CoordXRelative()+2);
           this.SetCoordYRelative(this.CoordYRelative()+2);
          }
        break;
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
        //--- shift the header to its original position and set the previous relative coordinates
        if(this.Move(this.CoordX(),this.CoordY()+2))
          {
           this.SetCoordXRelative(this.CoordXRelative());
           this.SetCoordYRelative(this.CoordYRelative()+2);
          }
        break;
      default:
        break;
     }
   return true;
  }
//+------------------------------------------------------------------+

Nach diesen Verbesserungen werden die Überschriften der Registerkarten auf der linken oder rechten Seite nun korrekt, wenn sie ausgewählt werden, vergrößert und verkleinert, wenn sie abgewählt werden, wobei sie visuell etwas größer werden und visuell an ihrem ursprünglichen Platz bleiben.

Wenn die Überschriften der Registerkarten in mehreren Reihen angeordnet sind, müssen wir bei der Auswahl einer Registerkarte, deren Überschrift nicht direkt an die Registerkarte selbst angrenzt, sondern irgendwo zwischen den Reihen der anderen Registerkarten liegt, die gesamte Reihe, in der sich die Überschrift der ausgewählten Registerkarte befindet, in die Nähe der Registerkartenfelder verschieben und die Zeile verschieben, die zuvor an die Felder angrenzte, um die Zeile mit der ausgewählten Überschrift zu ersetzen. Eine ähnliche Methode zur Platzierung von Registerkartenüberschriften über dem Steuerelement habe ich bereits in einem früheren Artikel implementiert. Nun müssen wir ähnliche Methoden für die Positionierung von Kopfzeilen am unteren, linken und rechten Rand entwickeln.

Die Methode, die die ausgewählte Registerkartenkopfleiste an die richtige Position am unteren Rand setzt:

//+------------------------------------------------------------------+
//| Set the row of a selected tab header                             |
//| to the correct position at the bottom                            |
//+------------------------------------------------------------------+
void CTabHeader::CorrectSelectedRowBottom(void)
  {
   int row_pressed=this.Row();      // Selected header row
   int y_pressed=this.CoordY();     // Coordinate where all headers with Row() equal to zero should be moved to
   int y0=0;                        // Zero row coordinate (Row == 0)
//--- If the zero row is selected, then nothing needs to be done - leave
   if(row_pressed==0)
      return;
      
//--- Get the tab field object corresponding to this header and set the Y coordinate of the zero line
   CWinFormBase *obj=this.GetFieldObj();
   if(obj==NULL)
      return;
   y0=obj.CoordY()+obj.Height();
   
//--- Get the base object (TabControl)
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return;
//--- Get the list of all tab headers from the base object
   CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);
   if(list==NULL)
      return;
//--- Swap rows in the loop through all headers -
//--- set the row of the selected header to the zero position, while the zero one is set to the position of the selected header row
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- If this is a zero row
      if(header.Row()==0)
        {
         //--- move the header to the position of the selected row
         if(header.Move(header.CoordX(),y_pressed))
           {
            header.SetCoordXRelative(header.CoordX()-base.CoordX());
            header.SetCoordYRelative(header.CoordY()-base.CoordY());
            //--- Set the Row value to -1. It will be used as a label of the moved zero row instead of the selected one
            header.SetRow(-1);
           }
        }
      //--- If this is the clicked header line,
      if(header.Row()==row_pressed)
        {
         //--- move the header to the position of the zero row
         if(header.Move(header.CoordX(),y0))
           {
            header.SetCoordXRelative(header.CoordX()-base.CoordX());
            header.SetCoordYRelative(header.CoordY()-base.CoordY());
            //--- Set the Row value to -2. It will be used as a label of the moved selected row instead of the zero one
            header.SetRow(-2);
           }
        }
     }
//--- Set the correct Row and Col
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- If this is the former zero row moved to the place of the selected one, set Row of the selected row to it
      if(header.Row()==-1)
         header.SetRow(row_pressed);
      //--- If this is the selected row moved to the zero position, set Row of the zero row
      if(header.Row()==-2)
         header.SetRow(0);
     }
  }
//+------------------------------------------------------------------+


Die Methode, die die ausgewählte Registerkarten-Kopfleiste an die richtige linke Position setzt:

//+------------------------------------------------------------------+
//| Set the row of a selected tab header                             |
//| to the correct position on the left                              |
//+------------------------------------------------------------------+
void CTabHeader::CorrectSelectedRowLeft(void)
  {
   int row_pressed=this.Row();      // Selected header row
   int x_pressed=this.CoordX();     // Coordinate where all headers with Row() equal to zero should be moved to
   int x0=0;                        // Zero row coordinate (Row == 0)
//--- If the zero row is selected, then nothing needs to be done - leave
   if(row_pressed==0)
      return;
      
//--- Get the tab field object corresponding to this header and set the X coordinate of the zero line
   CWinFormBase *obj=this.GetFieldObj();
   if(obj==NULL)
      return;
   x0=obj.CoordX()-this.Width()+2;
   
//--- Get the base object (TabControl)
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return;
//--- Get the list of all tab headers from the base object
   CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);
   if(list==NULL)
      return;
//--- Swap rows in the loop through all headers -
//--- set the row of the selected header to the zero position, while the zero one is set to the position of the selected header row
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- If this is a zero row
      if(header.Row()==0)
        {
         //--- move the header to the position of the selected row
         if(header.Move(x_pressed,header.CoordY()))
           {
            header.SetCoordXRelative(header.CoordX()-base.CoordX());
            header.SetCoordYRelative(header.CoordY()-base.CoordY());
            //--- Set the Row value to -1. It will be used as a label of the moved zero row instead of the selected one
            header.SetRow(-1);
           }
        }
      //--- If this is the clicked header line,
      if(header.Row()==row_pressed)
        {
         //--- move the header to the position of the zero row
         if(header.Move(x0,header.CoordY()))
           {
            header.SetCoordXRelative(header.CoordX()-base.CoordX());
            header.SetCoordYRelative(header.CoordY()-base.CoordY());
            //--- Set the Row value to -2. It will be used as a label of the moved selected row instead of the zero one
            header.SetRow(-2);
           }
        }
     }
//--- Set the correct Row and Col
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- If this is the former zero row moved to the place of the selected one, set Row of the selected row to it
      if(header.Row()==-1)
         header.SetRow(row_pressed);
      //--- If this is the selected row moved to the zero position, set Row of the zero row
      if(header.Row()==-2)
         header.SetRow(0);
     }
  }
//+------------------------------------------------------------------+


Die Methode, die die die ausgewählte Registerkarten-Kopfleiste an die richtige rechte Position setzt:

//+------------------------------------------------------------------+
//| Set the row of a selected tab header                             |
//| to the correct position on the right                             |
//+------------------------------------------------------------------+
void CTabHeader::CorrectSelectedRowRight(void)
  {
   int row_pressed=this.Row();      // Selected header row
   int x_pressed=this.CoordX();     // Coordinate where all headers with Row() equal to zero should be moved to
   int x0=0;                        // Zero row coordinate (Row == 0)
//--- If the zero row is selected, then nothing needs to be done - leave
   if(row_pressed==0)
      return;
      
//--- Get the tab field object corresponding to this header and set the X coordinate of the zero line
   CWinFormBase *obj=this.GetFieldObj();
   if(obj==NULL)
      return;
   x0=obj.RightEdge();
   
//--- Get the base object (TabControl)
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      return;
//--- Get the list of all tab headers from the base object
   CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER);
   if(list==NULL)
      return;
//--- Swap rows in the loop through all headers -
//--- set the row of the selected header to the zero position, while the zero one is set to the position of the selected header row
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- If this is a zero row
      if(header.Row()==0)
        {
         //--- move the header to the position of the selected row
         if(header.Move(x_pressed,header.CoordY()))
           {
            header.SetCoordXRelative(header.CoordX()-base.CoordX());
            header.SetCoordYRelative(header.CoordY()-base.CoordY());
            //--- Set the Row value to -1. It will be used as a label of the moved zero row instead of the selected one
            header.SetRow(-1);
           }
        }
      //--- If this is the clicked header line,
      if(header.Row()==row_pressed)
        {
         //--- move the header to the position of the zero row
         if(header.Move(x0,header.CoordY()))
           {
            header.SetCoordXRelative(header.CoordX()-base.CoordX());
            header.SetCoordYRelative(header.CoordY()-base.CoordY());
            //--- Set the Row value to -2. It will be used as a label of the moved selected row instead of the zero one
            header.SetRow(-2);
           }
        }
     }
//--- Set the correct Row and Col
   for(int i=0;i<list.Total();i++)
     {
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- If this is the former zero row moved to the place of the selected one, set Row of the selected row to it
      if(header.Row()==-1)
         header.SetRow(row_pressed);
      //--- If this is the selected row moved to the zero position, set Row of the zero row
      if(header.Row()==-2)
         header.SetRow(0);
     }
  }
//+------------------------------------------------------------------+

Im vorigen Artikel habe ich eine ähnliche Methode erwogen, nämlich das Verschieben einer Reihe von Kopfzeilen, wenn sie sich über dem Steuerelement befinden. Die Logik hinter diesen neuen Methoden ist genau dieselbe, aber für die untere Positionierung verschieben wir die Kopfzeilen entlang der Y-Achse, und für die linke und rechte Positionierung verschieben wir sie entlang der X-Achse. Die gesamte Logik ist in den Code-Kommentaren ausreichend detailliert beschrieben.


Wenn ein Tabulator durch Anklicken der Überschrift ausgewählt wird, wird die Überschrift etwas vergrößert und ggf. aus der Liste der Überschriftenzeilen in die Nähe des Tabulatorfeldes verschoben, und die dadurch entstehende sichtbare Grenze (durch den Feldrahmen) zwischen Überschrift und Tabulatorfeld wird gelöscht, sodass Feld und Überschrift wie ein untrennbares Ganzes wirken. Im vorherigen Artikel habe ich den Rand zwischen dem Feld und der Kopfzeile gelöscht, aber nur für die Position der Kopfzeile oben und unten. Jetzt müssen wir das Löschen des Randes zwischen dem Feld und der Kopfzeile hinzufügen, wenn sich letztere links und rechts befindet.

In \MQL5\Include\DoEasy\Objects\Graph\WForms\TabField.mqh der Tabulatorfeld-Objektklasse in der Methode zum Zeichnen des Kontrollrahmens in Abhängigkeit von der Position der Kopfzeile fügen wir das Zeichnen der Linie unter Verwendung der Hintergrundfarbe an der Position der Kopfzeile links und rechts hinzu:

//+------------------------------------------------------------------+
//| Draw the element frame depending on the header position          |
//+------------------------------------------------------------------+
void CTabField::DrawFrame(void)
  {
//--- Set the initial coordinates
   int x1=0;
   int y1=0;
   int x2=this.Width()-1;
   int y2=this.Height()-1;
//--- Get the tab header corresponding to the field
   CTabHeader *header=this.GetHeaderObj();
   if(header==NULL)
      return;
//--- Draw a rectangle that completely outlines the field
   this.DrawRectangle(x1,y1,x2,y2,this.BorderColor(),this.Opacity());
//--- Depending on the location of the header, draw a line on the edge adjacent to the header.
//--- The line size is calculated from the heading size and corresponds to it with a one-pixel indent on each side
//--- thus, visually the edge will not be drawn on the adjacent side of the header
   switch(header.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
        this.DrawLine(header.CoordXRelative()+1,0,header.RightEdgeRelative()-2,0,this.BackgroundColor(),this.Opacity());
        break;
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
        this.DrawLine(header.CoordXRelative()+1,this.Height()-1,header.RightEdgeRelative()-2,this.Height()-1,this.BackgroundColor(),this.Opacity());
        break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
        this.DrawLine(0,header.BottomEdgeRelative()-2,0,header.CoordYRelative()+1,this.BackgroundColor(),this.Opacity());
        break;
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
        this.DrawLine(this.Width()-1,header.BottomEdgeRelative()-2,this.Width()-1,header.CoordYRelative()+1,this.BackgroundColor(),this.Opacity());
        break;
      default:
        break;
     }
  }
//+------------------------------------------------------------------+

Hier ist alles ganz einfach. Wir holen uns den Zeiger auf das diesem Feld entsprechende Kopfzeilenobjekt und seine Größe und zeichnet eine Linie mit der Hintergrundfarbe in den Feldbereich, an den die Kopfzeile entsprechend der für die Kopfzeile angegebenen Position angrenzt. Dadurch wird die Grenze zwischen dem Feld und der Kopfzeile optisch aufgehoben, und die beiden Objekte erscheinen als ein einziges — die TabControl-Registerkarte, die ich weiter entwickeln werde.


In der TabControl-Objektklassendatei in \MQL5\Include\DoEasy\Objects\Graph\WForms\Containers\TabControl.mqh deklarieren wir vier private Methoden zum Dehnen von Kopfzeilen nach Breite und Höhe:

//--- Arrange the tab headers at the (1) top, (2) bottom, (3) left and (4) right
   void              ArrangeTabHeadersTop(void);
   void              ArrangeTabHeadersBottom(void);
   void              ArrangeTabHeadersLeft(void);
   void              ArrangeTabHeadersRight(void);
//--- Stretch tab headers by control size
   void              StretchHeaders(void);
//--- Stretch tab headers by (1) control width and height when positioned on the (2) left and (3) right
   void              StretchHeadersByWidth(void);
   void              StretchHeadersByHeightLeft(void);
   void              StretchHeadersByHeightRight(void);
public:


Wir implementieren diese Methoden außerhalb des Klassenkörpers.

Die Methode, mit der die Registerkartenüberschriften auf die Größe des Steuerelements gebracht werden:

//+------------------------------------------------------------------+
//| Stretch tab headers by control size                              |
//+------------------------------------------------------------------+
void CTabControl::StretchHeaders(void)
  {
//--- Leave if the headers are in one row
   if(!this.Multiline())
      return;
//--- Depending on the location of headers
   switch(this.Alignment())
     {
      case CANV_ELEMENT_ALIGNMENT_TOP     :
      case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
        this.StretchHeadersByWidth();
        break;
      case CANV_ELEMENT_ALIGNMENT_LEFT    :
        this.StretchHeadersByHeightLeft();
        break;
      case CANV_ELEMENT_ALIGNMENT_RIGHT   :
        this.StretchHeadersByHeightRight();
        break;
      default:
        break;
     }
  }
//+------------------------------------------------------------------+

Die Methode ruft einfach die entsprechenden Methoden auf, je nachdem, wo sich die Registerkartenüberschriften befinden. Für das Ausdehnung in der Breite reicht eine einzige Methode aus, da alle Kopfzeilen immer von links nach rechts angeordnet sind, während es für die Dehnung in der Höhe darauf ankommt, auf welcher Seite die Kopfzeilen angeordnet sind. Wenn sie sich auf der linken Seite befinden, sind sie von unten nach oben angeordnet, und wenn sie sich auf der linken Seite befinden, gehen sie von oben nach unten. Daher gibt es zwei getrennte Methoden zur Streckung nach Höhe für links und rechts positionierte Überschriften.

Die Methode zum Ausdehnen der Tabulatorüberschriften um die Kontrollbreite:

//+------------------------------------------------------------------+
//| Stretch tab headers by control width                             |
//+------------------------------------------------------------------+
void CTabControl::StretchHeadersByWidth(void)
  {
//--- Get the list of tab headers
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Get the last title in the list
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
   if(last==NULL)
      return;
//--- In the loop by the number of header rows
   for(int i=0;i<last.Row()+1;i++)
     {
      //--- Get the list with the row index equal to the loop index
      CArrayObj *list_row=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_ROW,i,EQUAL);
      if(list_row==NULL)
         continue;
      //--- Get the width of the container, as well as the number of headers in a row, and calculate the width of each header
      int base_size=this.Width()-4;
      int num=list_row.Total();
      int w=base_size/(num>0 ? num : 1);
      //--- In the loop by row headers
      for(int j=0;j<list_row.Total();j++)
        {
         //--- Get the current and previous headers from the list by loop index
         CTabHeader *header=list_row.At(j);
         CTabHeader *prev=list_row.At(j-1);
         if(header==NULL)
            continue;
         //--- If the header size is changed
         if(header.Resize(w,header.Height(),false))
           {
            //--- Set new sizes for the header for pressed/unpressed states
            header.SetWidthOn(w+4);
            header.SetWidthOff(w);
            //--- If this is the first header in the row (there is no previous header in the list),
            //--- then it is not necessary to shift it - move on to the next iteration
            if(prev==NULL)
               continue;
            //--- Shift the header to the coordinate of the right edge of the previous header
            if(header.Move(prev.RightEdge(),header.CoordY()))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

Hier wird zunächst die Anzahl der Kopfzeilen ermittelt. Diese Zahl lässt sich ermitteln, indem man die letzte Kopfzeile der Liste abruft — sie enthält den Index ihrer Zeile in der Eigenschaft Row. Da die Zeilenindizes bei Null beginnen, müssen wir zum resultierenden Wert eine Eins hinzufügen, um die Anzahl der Zeilen anzugeben.
Als Nächstes müssen wir eine Liste der Kopfzeilen in jeder Zeile abrufen und alle Kopfzeilen darin auf die Breite des Objekts strecken. Da wir den Objekteigenschaften Zeilen- und Spaltenwerte hinzugefügt haben, ist es recht einfach,eine Liste der Kopfzeilen einer Zeile zu erhalten — wir filtern die Liste aller Kopfzeilen nach dem Zeilenwert und erhalten eine Liste mit Zeigern auf Objekte mit der angegebenen Zeilennummer. In der Schleife durch die resultierende Liste ändern wir die Breite jeder Kopfzeile auf den zuvor berechneten Wert — die Breite des Containers geteilt durch die Anzahl der Kopfzeilen in der Zeile. Anstatt die gesamte Containerbreite zu verwenden, werden links und rechts zwei Pixel entfernt, sodass extreme Überschriften nicht über den Container hinausragen, wenn sie ausgewählt und vergrößert werden. Da wir die Größe im Voraus durch einen unbekannten Wert teilen, prüfen wir, ob der Divisor mit dem Wert übereinstimmt und teilen im Falle von 0 durch 1, um eine Division durch Null zu vermeiden. Wenn die vorherige Überschrift in der Liste nicht vorhanden ist (der Schleifenindex zeigt auf die allererste Überschrift), muss die Überschrift nirgendwo verschoben werden. Sie bleibt an ihrem Platz, während alle nachfolgenden Kopfzeilen an den rechten Rand der vorherigen Kopfzeile verschoben werden müssen — alle Kopfzeilen haben ihre Breite verändert, sind größer geworden und überlappen sich gegenseitig.

Die Methode, mit der die Registerkartenüberschriften gestreckt werden, damit sie in die Höhe des Steuerelements passen, wenn dieses nach links positioniert ist:

//+------------------------------------------------------------------+
//| Stretch tab headers by control height                            |
//| when placed on the left                                          |
//+------------------------------------------------------------------+
void CTabControl::StretchHeadersByHeightLeft(void)
  {
//--- Get the list of tab headers
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Get the last title in the list
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
   if(last==NULL)
      return;
//--- In the loop by the number of header rows
   for(int i=0;i<last.Row()+1;i++)
     {
      //--- Get the list with the row index equal to the loop index
      CArrayObj *list_row=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_ROW,i,EQUAL);
      if(list_row==NULL)
         continue;
      //--- Get the height of the container, as well as the number of headers in a row, and calculate the height of each header
      int base_size=this.Height()-4;
      int num=list_row.Total();
      int h=base_size/(num>0 ? num : 1);
      //--- In the loop by row headers
      for(int j=0;j<list_row.Total();j++)
        {
         //--- Get the current and previous headers from the list by loop index
         CTabHeader *header=list_row.At(j);
         CTabHeader *prev=list_row.At(j-1);
         if(header==NULL)
            continue;
         //--- Save the initial header height
         int h_prev=header.Height();
         //--- If the header size is changed
         if(header.Resize(header.Width(),h,false))
           {
            //--- Set new sizes for the header for pressed/unpressed states
            header.SetHeightOn(h+4);
            header.SetHeightOff(h);
            //--- If this is the first header in the row (there is no previous header in the list)
            if(prev==NULL)
              {
               //--- Calculate the Y offset
               int y_shift=header.Height()-h_prev;
               //--- Shift the header by its calculated offset and move on to the next one
               if(header.Move(header.CoordX(),header.CoordY()-y_shift))
                 {
                  header.SetCoordXRelative(header.CoordX()-this.CoordX());
                  header.SetCoordYRelative(header.CoordY()-this.CoordY());
                 }
               continue;
              }
            //--- Move the header by the coordinate of the top edge of the previous header minus the height of the current one and its calculated offset
            if(header.Move(header.CoordX(),prev.CoordY()-header.Height()))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

Die Logik der Methode ist ähnlich wie bei der vorhergehenden, obwohl sie hier etwas komplizierter ist. Da Kopfzeilen von der unteren Kante ihres Containers aus nach links positioniert werden und der Ankerpunkt der Kopfzeile in der oberen linken Ecke liegt, führt eine Größenänderung dazu, dass die untere Kante der Kopfzeile unterhalb der unteren Kante des Containers liegt. Daher müssen wir hier die allererste Überschrift um den berechneten Versatz nach oben verschieben. Dazu müssen wir die Höhe der Kopfzeile speichern, bevor wir sie ändern, und berechnen, um wie viel sich die Größe nach der Größenänderung verändert hat. Wir verwenden den erhaltenen Wert, um die allererste Kopfzeile entlang der Y-Achse zu verschieben, sodass ihr unterer Rand nicht über ihren Container hinausragt.


Die Methode, mit der die Registerkartenüberschriften gestreckt werden, damit sie in die Höhe des Steuerelements passen, wenn dieses nach rechts verschoben wird:

//+------------------------------------------------------------------+
//| Stretch tab headers by control height                            |
//| when placed on the right                                         |
//+------------------------------------------------------------------+
void CTabControl::StretchHeadersByHeightRight(void)
  {
//--- Get the list of tab headers
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Get the last title in the list
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
   if(last==NULL)
      return;
//--- In the loop by the number of header rows
   for(int i=0;i<last.Row()+1;i++)
     {
      //--- Get the list with the row index equal to the loop index
      CArrayObj *list_row=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_TAB_PAGE_ROW,i,EQUAL);
      if(list_row==NULL)
         continue;
      //--- Get the height of the container, as well as the number of headers in a row, and calculate the height of each header
      int base_size=this.Height()-4;
      int num=list_row.Total();
      int h=base_size/(num>0 ? num : 1);
      //--- In the loop by row headers
      for(int j=0;j<list_row.Total();j++)
        {
         //--- Get the current and previous headers from the list by loop index
         CTabHeader *header=list_row.At(j);
         CTabHeader *prev=list_row.At(j-1);
         if(header==NULL)
            continue;
         //--- If the header size is changed
         if(header.Resize(header.Width(),h,false))
           {
            //--- Set new sizes for the header for pressed/unpressed states
            header.SetHeightOn(h+4);
            header.SetHeightOff(h);
            //--- If this is the first header in the row (there is no previous header in the list),
            //--- then it is not necessary to shift it - move on to the next iteration
            if(prev==NULL)
               continue;
            //--- Shift the header to the coordinate of the bottom edge of the previous header
            if(header.Move(header.CoordX(),prev.BottomEdge()))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

Die Methode ist identisch mit der, bei der die Kopfzeilen auf die Breite des Containers gestreckt werden, aber hier werden sie in der Höhe gestreckt. Da sich hier die Kopfzeilen auf der linken Seite befinden und ihr Bericht von oben nach unten verläuft, brauchen wir die Position der ersten Kopfzeile nach der Änderung ihrer Größe nicht anzupassen — ihre Anfangskoordinaten stimmen mit den Koordinaten ihres Platzierungspunkts überein, und das Objekt wird nach unten wachsen, ohne über den Container hinauszuwachsen.

Die Methode, mit der die angegebene Anzahl von Registerkarten erstellt wird, wurde geändert, da wir die Anfangskoordinaten und -größen anhand der Position der Kopfzeilen berechnen müssen. Um die Kopfzeilen links und rechts zu platzieren, weisen wir die der Methode übergebene Kopfzeilenhöhe und -breite der Breite und Höhe entsprechend zu. Wenn sich die Kopfzeile auf der linken Seite befindet, drehen wir die Kopfzeile vertikal um 90°, wenn sie sich auf der rechten Seite befindet — um 270°:

//+------------------------------------------------------------------+
//| Create the specified number of tabs                              |
//+------------------------------------------------------------------+
bool CTabControl::CreateTabPages(const int total,const int selected_page,const int tab_w=0,const int tab_h=0,const string header_text="")
  {
//--- Calculate the size and initial coordinates of the tab title
   int w=(tab_w==0 ? this.ItemWidth()  : tab_w);
   int h=(tab_h==0 ? this.ItemHeight() : tab_h);

//--- In the loop by the number of tabs
   CTabHeader *header=NULL;
   CTabField  *field=NULL;
   for(int i=0;i<total;i++)
     {
      //--- Depending on the location of tab titles, set their initial coordinates
      int header_x=2;
      int header_y=0;
      int header_w=w;
      int header_h=h;
      
      //--- Set the current X and Y coordinate depending on the location of the tab headers
      switch(this.Alignment())
        {
         case CANV_ELEMENT_ALIGNMENT_TOP     :
           header_w=w;
           header_h=h;
           header_x=(header==NULL ? 2 : header.RightEdgeRelative());
           header_y=0;
           break;
         case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
           header_w=w;
           header_h=h;
           header_x=(header==NULL ? 2 : header.RightEdgeRelative());
           header_y=this.Height()-header_h;
           break;
         case CANV_ELEMENT_ALIGNMENT_LEFT    :
           header_w=h;
           header_h=w;
           header_x=2;
           header_y=(header==NULL ? this.Height()-header_h-2 : header.CoordYRelative()-header_h);
           break;
         case CANV_ELEMENT_ALIGNMENT_RIGHT   :
           header_w=h;
           header_h=w;
           header_x=this.Width()-header_w;
           header_y=(header==NULL ? 2 : header.BottomEdgeRelative());
           break;
         default:
           break;
        }
      //--- Create the TabHeader object
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,header_x,header_y,header_w,header_h,clrNONE,255,this.Active(),false))
        {
         ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER),string(i+1));
         return false;
        }
      header=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER,i);
      if(header==NULL)
        {
         ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_HEADER),string(i+1));
         return false;
        }
      header.SetBase(this.GetObject());
      header.SetPageNumber(i);
      header.SetGroup(this.Group()+1);
      header.SetBackgroundColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR,true);
      header.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_DOWN);
      header.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_MOUSE_OVER);
      header.SetBackgroundStateOnColor(CLR_DEF_CONTROL_TAB_HEAD_BACK_COLOR_ON,true);
      header.SetBackgroundStateOnColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BACK_DOWN_ON);
      header.SetBackgroundStateOnColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BACK_OVER_ON);
      header.SetBorderStyle(FRAME_STYLE_SIMPLE);
      header.SetBorderColor(CLR_DEF_CONTROL_TAB_HEAD_BORDER_COLOR,true);
      header.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_DOWN);
      header.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_HEAD_BORDER_MOUSE_OVER);
      header.SetAlignment(this.Alignment());
      header.SetPadding(this.HeaderPaddingWidth(),this.HeaderPaddingHeight(),this.HeaderPaddingWidth(),this.HeaderPaddingHeight());
      if(header_text!="" && header_text!=NULL)
         this.SetHeaderText(header,header_text+string(i+1));
      else
         this.SetHeaderText(header,"TabPage"+string(i+1));
      if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT)
         header.SetFontAngle(90);
      if(this.Alignment()==CANV_ELEMENT_ALIGNMENT_RIGHT)
         header.SetFontAngle(270);
      header.SetTabSizeMode(this.TabSizeMode());

      
      //--- Save the initial height of the header and set its size in accordance with the header size setting mode
      int h_prev=header_h;
      header.SetSizes(header_w,header_h);
      //--- Get the Y offset of the header position after changing its height and
      //--- shift it by the calculated value only for headers on the left
      int y_shift=header.Height()-h_prev;
      if(header.Move(header.CoordX(),header.CoordY()-(this.Alignment()==CANV_ELEMENT_ALIGNMENT_LEFT ? y_shift : 0)))
        {
         header.SetCoordXRelative(header.CoordX()-this.CoordX());
         header.SetCoordYRelative(header.CoordY()-this.CoordY());
        }
      
      //--- Depending on the location of the tab headers, set the initial coordinates of the tab fields
      int field_x=0;
      int field_y=0;
      int field_w=this.Width();
      int field_h=this.Height()-header.Height();
      int header_shift=0;
      
      switch(this.Alignment())
        {
         case CANV_ELEMENT_ALIGNMENT_TOP     :
           field_x=0;
           field_y=header.BottomEdgeRelative();
           field_w=this.Width();
           field_h=this.Height()-header.Height();
           break;
         case CANV_ELEMENT_ALIGNMENT_BOTTOM  :
           field_x=0;
           field_y=0;
           field_w=this.Width();
           field_h=this.Height()-header.Height();
           break;
         case CANV_ELEMENT_ALIGNMENT_LEFT    :
           field_x=header.RightEdgeRelative();
           field_y=0;
           field_h=this.Height();
           field_w=this.Width()-header.Width();
           break;
         case CANV_ELEMENT_ALIGNMENT_RIGHT   :
           field_x=0;
           field_y=0;
           field_h=this.Height();
           field_w=this.Width()-header.Width();
           break;
         default:
           break;
        }
      
      //--- Create the TabField object (tab field)
      if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,field_x,field_y,field_w,field_h,clrNONE,255,true,false))
        {
         ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD),string(i+1));
         return false;
        }
      field=this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD,i);
      if(field==NULL)
        {
         ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_TAB_FIELD),string(i+1));
         return false;
        }
      field.SetBase(this.GetObject());
      field.SetPageNumber(i);
      field.SetGroup(this.Group()+1);
      field.SetBorderSizeAll(1);
      field.SetBorderStyle(FRAME_STYLE_SIMPLE);
      field.SetOpacity(CLR_DEF_CONTROL_TAB_PAGE_OPACITY,true);
      field.SetBackgroundColor(CLR_DEF_CONTROL_TAB_PAGE_BACK_COLOR,true);
      field.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_DOWN);
      field.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_MOUSE_OVER);
      field.SetBorderColor(CLR_DEF_CONTROL_TAB_PAGE_BORDER_COLOR,true);
      field.SetBorderColorMouseDown(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_DOWN);
      field.SetBorderColorMouseOver(CLR_DEF_CONTROL_TAB_PAGE_BORDER_MOUSE_OVER);
      field.SetForeColor(CLR_DEF_FORE_COLOR,true);
      field.SetPadding(this.FieldPaddingLeft(),this.FieldPaddingTop(),this.FieldPaddingRight(),this.FieldPaddingBottom());
      field.Hide();
     }
//--- Arrange all titles in accordance with the specified display modes and select the specified tab
   this.ArrangeTabHeaders();
   this.Select(selected_page,true);
   return true;
  }
//+------------------------------------------------------------------+

Die Logik der Methode und die Verbesserungen sind in den Kommentaren zum Code beschrieben, und die wichtigsten Merkmale sind vor der Auflistung der Methode angegeben und farblich hervorgehoben. Um die Kopfzeilen auf der linken Seite zu platzieren, müssen wir die Größe der Kopfzeilen speichern, bevor wir sie ändern, den Versatz berechnen und die in der Größe veränderte Kopfzeile an die richtige Position verschieben.

Die im vorigen Artikel beschriebene Methode, die Tabulatorüberschriften oben zu platzieren, wurde ebenfalls geändert:

//+------------------------------------------------------------------+
//| Arrange tab headers on top                                       |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeadersTop(void)
  {
//--- Get the list of tab headers
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Declare the variables
   int col=0;                                // Column
   int row=0;                                // Row
   int x1_base=2;                            // Initial X coordinate
   int x2_base=this.RightEdgeRelative()-2;   // Final X coordinate
   int x_shift=0;                            // Shift the tab set for calculating their exit beyond the container
   int n=0;                                  // The variable for calculating the column index relative to the loop index
//--- In a loop by the list of headers,
   for(int i=0;i<list.Total();i++)
     {
      //--- get the next tab header object
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- If the flag for positioning headers in several rows is set
      if(this.Multiline())
        {
         //--- Calculate the value of the right edge of the header, taking into account that
         //--- the origin always comes from the left edge of TabControl + 2 pixels
         int x2=header.RightEdgeRelative()-x_shift;
         //--- If the calculated value does not go beyond the right edge of the TabControl minus 2 pixels, 
         //--- set the column number equal to the loop index minus the value in the n variable
         if(x2<x2_base)
            col=i-n;
         //--- If the calculated value goes beyond the right edge of the TabControl minus 2 pixels,
         else
           {
            //--- Increase the row index, calculate the new shift (so that the next object is compared with the TabControl left edge + 2 pixels),
            //--- set the loop index for the n variable, while the column index is set to zero, this is the start of the new row
            row++;
            x_shift=header.CoordXRelative()-2;
            n=i;
            col=0;
           }
         //--- Assign the row and column indices to the tab header and shift it to the calculated coordinates
         header.SetTabLocation(row,col);
         if(header.Move(header.CoordX()-x_shift,header.CoordY()-header.Row()*header.Height()))
           {
            header.SetCoordXRelative(header.CoordX()-this.CoordX());
            header.SetCoordYRelative(header.CoordY()-this.CoordY());
           }
        }
      //--- If only one row of headers is allowed
      else
        {
         
        }
     }

//--- The location of all tab titles is set. Now place them all together with the fields
//--- according to the header row and column indices.

//--- Get the last title in the list
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
//--- If the object is received
   if(last!=NULL)
     {
      //--- If the mode of stretching headers to the width of the container is set, call the stretching method
      if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL)
         this.StretchHeaders();
      //--- If this is not the first row (with index 0)
      if(last.Row()>0)
        {
         //--- Calculate the offset of the tab field Y coordinate
         int y_shift=last.Row()*last.Height();
         //--- In a loop by the list of headers,
         for(int i=0;i<list.Total();i++)
           {
            //--- get the next object
            CTabHeader *header=list.At(i);
            if(header==NULL)
               continue;
            //--- get the tab field corresponding to the received header
            CTabField  *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- shift the tab header by the calculated row coordinates
            if(header.Move(header.CoordX(),header.CoordY()+y_shift))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
              }
            //--- shift the tab field by the calculated shift
            if(field.Move(field.CoordX(),field.CoordY()+y_shift))

              {
               field.SetCoordXRelative(field.CoordX()-this.CoordX());
               field.SetCoordYRelative(field.CoordY()-this.CoordY());
               //--- change the size of the shifted field by the value of its shift
               field.Resize(field.Width(),field.Height()-y_shift,false);
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

Die Logik der Methode ist in den Kommentaren zum Code ausführlich beschrieben. Ich werde sie hier nicht wiederholen.


Die Methode, die Registerkartenüberschriften am unteren Rand platziert:

//+------------------------------------------------------------------+
//| Arrange tab headers at the bottom                                |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeadersBottom(void)
  {
//--- Get the list of tab headers
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Declare the variables
   int col=0;                                // Column
   int row=0;                                // Row
   int x1_base=2;                            // Initial X coordinate
   int x2_base=this.RightEdgeRelative()-2;   // Final X coordinate
   int x_shift=0;                            // Shift the tab set for calculating their exit beyond the container
   int n=0;                                  // The variable for calculating the column index relative to the loop index
//--- In a loop by the list of headers,
   for(int i=0;i<list.Total();i++)
     {
      //--- get the next tab header object
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- If the flag for positioning headers in several rows is set
      if(this.Multiline())
        {
         //--- Calculate the value of the right edge of the header, taking into account that
         //--- the origin always comes from the left edge of TabControl + 2 pixels
         int x2=header.RightEdgeRelative()-x_shift;
         //--- If the calculated value does not go beyond the right edge of the TabControl minus 2 pixels, 
         //--- set the column number equal to the loop index minus the value in the n variable
         if(x2<x2_base)
            col=i-n;
         //--- If the calculated value goes beyond the right edge of the TabControl minus 2 pixels,
         else
           {
            //--- Increase the row index, calculate the new shift (so that the next object is compared with the TabControl left edge + 2 pixels),
            //--- set the loop index for the n variable, while the column index is set to zero, this is the start of the new row
            row++;
            x_shift=header.CoordXRelative()-2;
            n=i;
            col=0;
           }
         //--- Assign the row and column indices to the tab header and shift it to the calculated coordinates
         header.SetTabLocation(row,col);
         if(header.Move(header.CoordX()-x_shift,header.CoordY()+header.Row()*header.Height()))
           {
            header.SetCoordXRelative(header.CoordX()-this.CoordX());
            header.SetCoordYRelative(header.CoordY()-this.CoordY());
           }
        }
      //--- If only one row of headers is allowed
      else
        {
         
        }
     }

//--- The location of all tab titles is set. Now place them all together with the fields
//--- according to the header row and column indices.

//--- Get the last title in the list
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
//--- If the object is received
   if(last!=NULL)
     {
      //--- If the mode of stretching headers to the width of the container is set, call the stretching method
      if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL)
         this.StretchHeaders();
      //--- If this is not the first row (with index 0)
      if(last.Row()>0)
        {
         //--- Calculate the offset of the tab field Y coordinate
         int y_shift=last.Row()*last.Height();
         //--- In a loop by the list of headers,
         for(int i=0;i<list.Total();i++)
           {
            //--- get the next object
            CTabHeader *header=list.At(i);
            if(header==NULL)
               continue;
            //--- get the tab field corresponding to the received header
            CTabField  *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- shift the tab header by the calculated row coordinates
            if(header.Move(header.CoordX(),header.CoordY()-y_shift))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
              }
            //--- shift the tab field by the calculated shift
            if(field.Move(field.CoordX(),field.CoordY()))
              {
               field.SetCoordXRelative(field.CoordX()-this.CoordX());
               field.SetCoordYRelative(field.CoordY()-this.CoordY());
               //--- change the size of the shifted field by the value of its shift
               field.Resize(field.Width(),field.Height()-y_shift,false);
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

Die Methode ist identisch mit der Methode, bei der die Kopfzeilen oben stehen. Der einzige Unterschied besteht in der Richtung des Versatzes der Kopfzeilen, da sich die Kopfzeilen am unteren Rand befinden und sich im Vergleich zur vorherigen Methode in umgekehrter Richtung bewegen.


Die Methode zur Lokalisierung Registerkartenüberschriften auf der linken Seite:

//+------------------------------------------------------------------+
//| Arrange tab headers on the left                                  |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeadersLeft(void)
  {
//--- Get the list of tab headers
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Declare the variables
   int col=0;                                // Column
   int row=0;                                // Row
   int y1_base=this.BottomEdgeRelative()-2;  // Initial Y coordinate
   int y2_base=2;                            // Final Y coordinate
   int y_shift=0;                            // Shift the tab set for calculating their exit beyond the container
   int n=0;                                  // The variable for calculating the column index relative to the loop index
//--- In a loop by the list of headers,
   for(int i=0;i<list.Total();i++)
     {
      //--- get the next tab header object
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- If the flag for positioning headers in several rows is set
      if(this.Multiline())
        {
         //--- Calculate the value of the upper edge of the header, taking into account that
         //--- the origin always comes from the bottom edge of TabControl minus 2 pixels
         int y2=header.CoordYRelative()+y_shift;
         //--- If the calculated value does not go beyond the upper edge of the TabControl minus 2 pixels, 
         //--- set the column number equal to the loop index minus the value in the n variable
         if(y2>=y2_base)
            col=i-n;
         //--- If the calculated value goes beyond the upper edge of the TabControl minus 2 pixels,
         else
           {
            //--- Increase the row index, calculate the new shift (so that the next object is compared with the TabControl left edge + 2 pixels),
            //--- set the loop index for the n variable, while the column index is set to zero, this is the start of the new row
            row++;
            y_shift=this.BottomEdge()-header.BottomEdge()-2;
            n=i;
            col=0;
           }
         //--- Assign the row and column indices to the tab header and shift it to the calculated coordinates
         header.SetTabLocation(row,col);
         if(header.Move(header.CoordX()-header.Row()*header.Width(),header.CoordY()+y_shift))
           {
            header.SetCoordXRelative(header.CoordX()-this.CoordX());
            header.SetCoordYRelative(header.CoordY()-this.CoordY());
           }
        }
      //--- If only one row of headers is allowed
      else
        {
         
        }
     }

//--- The location of all tab titles is set. Now place them all together with the fields
//--- according to the header row and column indices.

//--- Get the last title in the list
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
//--- If the object is received
   if(last!=NULL)
     {
      //--- If the mode of stretching headers to the width of the container is set, call the stretching method
      if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL)
         this.StretchHeaders();
      //--- If this is not the first row (with index 0)
      if(last.Row()>0)
        {
         //--- Calculate the offset of the tab field X coordinate
         int x_shift=last.Row()*last.Width();
         //--- In a loop by the list of headers,
         for(int i=0;i<list.Total();i++)
           {
            //--- get the next object
            CTabHeader *header=list.At(i);
            if(header==NULL)
               continue;
            //--- get the tab field corresponding to the received header
            CTabField  *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- shift the tab header by the calculated row coordinates
            if(header.Move(header.CoordX()+x_shift,header.CoordY()))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
              }
            //--- shift the tab field by the calculated shift
            if(field.Move(field.CoordX()+x_shift,field.CoordY()))
              {
               field.SetCoordXRelative(field.CoordX()-this.CoordX());
               field.SetCoordYRelative(field.CoordY()-this.CoordY());
               //--- change the size of the shifted field by the value of its shift
               field.Resize(field.Width()-x_shift,field.Height(),false);
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

Hier befinden sich die Kopfzeilen auf der linken Seite und die Zeilen sind entlang der X-Achse verschoben. Ansonsten ist die Logik identisch mit den vorherigen Methoden.


Die Methode, die Tabulatorüberschriften nach rechts positioniert:

//+------------------------------------------------------------------+
//| Arrange tab headers to the right                                 |
//+------------------------------------------------------------------+
void CTabControl::ArrangeTabHeadersRight(void)
  {
//--- Get the list of tab headers
   CArrayObj *list=this.GetListHeaders();
   if(list==NULL)
      return;
//--- Declare the variables
   int col=0;                                // Column
   int row=0;                                // Row
   int y1_base=2;                            // Initial Y coordinate
   int y2_base=this.BottomEdgeRelative()-2;  // Final Y coordinate
   int y_shift=0;                            // Shift the tab set for calculating their exit beyond the container
   int n=0;                                  // The variable for calculating the column index relative to the loop index
//--- In a loop by the list of headers,
   for(int i=0;i<list.Total();i++)
     {
      //--- get the next tab header object
      CTabHeader *header=list.At(i);
      if(header==NULL)
         continue;
      //--- If the flag for positioning headers in several rows is set
      if(this.Multiline())
        {
         //--- Calculate the value of the bottom edge of the header, taking into account that
         //--- the origin always comes from the upper edge of TabControl + 2 pixels
         int y2=header.BottomEdgeRelative()-y_shift;
         //--- If the calculated value does not go beyond the bottom edge of the TabControl minus 2 pixels, 
         //--- set the column number equal to the loop index minus the value in the n variable
         if(y2<y2_base)
            col=i-n;
         //--- If the calculated value goes beyond the bottom edge of the TabControl minus 2 pixels,
         else
           {
            //--- Increase the row index, calculate the new shift (so that the next object is compared with the TabControl bottom edge + 2 pixels),
            //--- set the loop index for the n variable, while the column index is set to zero, this is the start of the new row
            row++;
            y_shift=header.CoordYRelative()-2;
            n=i;
            col=0;
           }
         //--- Assign the row and column indices to the tab header and shift it to the calculated coordinates
         header.SetTabLocation(row,col);
         if(header.Move(header.CoordX()+header.Row()*header.Width(),header.CoordY()-y_shift))
           {
            header.SetCoordXRelative(header.CoordX()-this.CoordX());
            header.SetCoordYRelative(header.CoordY()-this.CoordY());
           }
        }
      //--- If only one row of headers is allowed
      else
        {
         
        }
     }

//--- The location of all tab titles is set. Now place them all together with the fields
//--- according to the header row and column indices.

//--- Get the last title in the list
   CTabHeader *last=this.GetTabHeader(list.Total()-1);
//--- If the object is received
   if(last!=NULL)
     {
      //--- If the mode of stretching headers to the width of the container is set, call the stretching method
      if(this.TabSizeMode()==CANV_ELEMENT_TAB_SIZE_MODE_FILL)
         this.StretchHeaders();
      //--- If this is not the first row (with index 0)
      if(last.Row()>0)
        {
         //--- Calculate the offset of the tab field X coordinate
         int x_shift=last.Row()*last.Width();
         //--- In a loop by the list of headers,
         for(int i=0;i<list.Total();i++)
           {
            //--- get the next object
            CTabHeader *header=list.At(i);
            if(header==NULL)
               continue;
            //--- get the tab field corresponding to the received header
            CTabField  *field=header.GetFieldObj();
            if(field==NULL)
               continue;
            //--- shift the tab header by the calculated row coordinates
            if(header.Move(header.CoordX()-x_shift,header.CoordY()))
              {
               header.SetCoordXRelative(header.CoordX()-this.CoordX());
               header.SetCoordYRelative(header.CoordY()-this.CoordY());
               //--- change the tab field size to the X offset value
               field.Resize(field.Width()-x_shift,field.Height(),false);
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

Die Logik ist ähnlich wie bei der vorherigen Methode, aber die Zeilenversätze sind gespiegelt, weil die Kopfzeilen rechts stehen.

Alle oben genannten Methoden sind im Code ausführlich kommentiert — wir überlassen sie einem unabhängigem Studium. Wenn Sie Fragen haben, können Sie diese gerne im Kommentarteil stellen.

Jetzt können wir alle Änderungen und Verbesserungen testen. Um die Überschriften der Registerkarten in einer Reihe anzuordnen, müssen wir den sichtbaren/unsichtbaren Teil des grafischen Elements beschneiden können. Wenn wir also den Modus für die Platzierung von Kopfzeilen in einer Zeile wählen (der Mehrzeilenmodus ist ausgeschaltet), während es viele Registerkarten gibt, werden alle Kopfzeilen über das Steuerelement hinaus aufgereiht. Ich werde mich mit diesem Thema in späteren Artikeln befassen. Ich habe den „Stopfen“ für diesen Modus in den Methoden der besprochenen Klassen gelassen. Ich werde dort den Code für die Handhabung dieses Modus eingeben.

Test

Um den Test durchzuführen, verwende ich den EA aus dem vorherigen Artikel und speichere ihn in \MQL5\Experts\TestDoEasy\Part116\ als TestDoEasy116.mq5.

In den EA-Eingaben fügen wir die Variablen hinzu, um den Mehrzeilenmodus und die Seite, auf der die Tabulatorüberschriften platziert werden, anzugeben:

//--- input parameters
sinput   bool                          InpMovable           =  true;                   // Panel Movable flag
sinput   ENUM_INPUT_YES_NO             InpAutoSize          =  INPUT_YES;              // Panel Autosize
sinput   ENUM_AUTO_SIZE_MODE           InpAutoSizeMode      =  AUTO_SIZE_MODE_GROW;    // Panel Autosize mode
sinput   ENUM_BORDER_STYLE             InpFrameStyle        =  BORDER_STYLE_SIMPLE;    // Label border style
sinput   ENUM_ANCHOR_POINT             InpTextAlign         =  ANCHOR_CENTER;          // Label text align
sinput   ENUM_INPUT_YES_NO             InpTextAutoSize      =  INPUT_NO;               // Label autosize
sinput   ENUM_ANCHOR_POINT             InpCheckAlign        =  ANCHOR_LEFT;            // Check flag align
sinput   ENUM_ANCHOR_POINT             InpCheckTextAlign    =  ANCHOR_LEFT;            // Check label text align
sinput   ENUM_CHEK_STATE               InpCheckState        =  CHEK_STATE_UNCHECKED;   // Check flag state
sinput   ENUM_INPUT_YES_NO             InpCheckAutoSize     =  INPUT_YES;              // CheckBox autosize
sinput   ENUM_BORDER_STYLE             InpCheckFrameStyle   =  BORDER_STYLE_NONE;      // CheckBox border style
sinput   ENUM_ANCHOR_POINT             InpButtonTextAlign   =  ANCHOR_CENTER;          // Button text align
sinput   ENUM_INPUT_YES_NO             InpButtonAutoSize    =  INPUT_YES;              // Button autosize
sinput   ENUM_AUTO_SIZE_MODE           InpButtonAutoSizeMode=  AUTO_SIZE_MODE_GROW;    // Button Autosize mode
sinput   ENUM_BORDER_STYLE             InpButtonFrameStyle  =  BORDER_STYLE_NONE;      // Button border style
sinput   bool                          InpButtonToggle      =  true ;                  // Button toggle flag
sinput   bool                          InpButtListMSelect   =  false;                  // ButtonListBox Button MultiSelect flag
sinput   bool                          InpListBoxMColumn    =  true;                   // ListBox MultiColumn flag
sinput   bool                          InpTabCtrlMultiline  =  true;                   // Tab Control Multiline flag
sinput   ENUM_ELEMENT_ALIGNMENT        InpHeaderAlignment   =  ELEMENT_ALIGNMENT_TOP;  // TabHeader Alignment
sinput   ENUM_ELEMENT_TAB_SIZE_MODE    InpTabPageSizeMode   =  ELEMENT_TAB_SIZE_MODE_NORMAL; // TabHeader Size Mode
//--- global variables


Erhöhen wir die Breite des erstellten Bereichs leicht (um 10 Pixel):

//--- Create WinForms Panel object
   CPanel *pnl=NULL;
   pnl=engine.CreateWFPanel("WFPanel",50,50,410,200,array_clr,200,true,true,false,-1,FRAME_STYLE_BEVEL,true,false);
   if(pnl!=NULL)
     {

und die Breite des zweiten GroupBox-Containers — um 12 Pixel:

      //--- Create the GroupBox2 WinForms object
      CGroupBox *gbox2=NULL;
      //--- The indent from the attached panels by 6 pixels will be the Y coordinate of GrotupBox2
      w=gbox1.Width()+12;
      int x=gbox1.RightEdgeRelative()+1;
      int h=gbox1.BottomEdgeRelative()-6;
      //--- If the attached GroupBox object is created
      if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,x,2,w,h,C'0x91,0xAA,0xAE',0,true,false))
        {

All dies geschieht nur, weil wir nicht die Möglichkeit haben, den unsichtbaren Teil der grafischen Elemente zu beschneiden, und alle WinForm-Objekte, die sich auf ihren übergeordneten Objekten befinden und deren Größe größer ist als die des Containers (übergeordnetes Objekt), werden über dessen Grenzen hinausgehen. So wird z. B. eine CheckBox, die auf dem Registerkartenfeld platziert ist, entweder über das Registerkartenfeld hinausragen, wenn sich die Überschriften auf der linken Seite befinden, oder auch über das Registerkartenfeld hinausgehen und die Registerkartenüberschriften auf der rechten Seite von TabControl abdecken. Es gibt zwar noch nicht genug Funktionalität, aber solche Mängel muss ich ausblenden :)

In OnInit() legen wir nach dem Erstellen von TabControl die Position der Registerkartenüberschriften und die Erlaubnis für die Anordnung der Überschriften in mehreren Zeilen fest, die in den EA-Eingaben angegeben sind:

            //--- Create the TabControl object
            gbox2.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,4,12,gbox2.Width()-12,gbox2.Height()-20,clrNONE,255,true,false);
            //--- get the pointer to the TabControl object by its index in the list of bound objects of the TabControl type
            CTabControl *tab_ctrl=gbox2.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,0);
            //--- If TabControl is created and the pointer to it is received
            if(tab_ctrl!=NULL)
              {
               //--- Set the location of the tab titles on the element and the tab text, as well as create nine tabs
               tab_ctrl.SetTabSizeMode((ENUM_CANV_ELEMENT_TAB_SIZE_MODE)InpTabPageSizeMode);
               tab_ctrl.SetAlignment((ENUM_CANV_ELEMENT_ALIGNMENT)InpHeaderAlignment);
               tab_ctrl.SetMultiline(InpTabCtrlMultiline);
               tab_ctrl.SetHeaderPadding(6,0);
               tab_ctrl.CreateTabPages(9,0,50,16,TextByLanguage("Вкладка","TabPage"));


Wenn wir das Steuerelement ListBox auf der dritten Registerkarte von TabControl erstellen, setzen wir seine Y-Koordinate näher an den oberen Rand der Registerkarte:

               //--- Create the ListBox object on the third tab
               int lbw=146;
               if(!InpListBoxMColumn)
                  lbw=100;
               tab_ctrl.CreateNewElement(2,GRAPH_ELEMENT_TYPE_WF_LIST_BOX,4,2,lbw,60,clrNONE,255,true,false);
               //--- get the pointer to the ListBox object from the third tab by its index in the list of attached objects of the ListBox type

Bisher befand sich das Objekt an der Koordinate 12, was dazu führt, dass es bei einer mehrzeiligen Anordnung von Tabulatorüberschriften von unten über das Tabulatorfeld hinausgeht (da die Größe des Tabulatorfeldes mit zunehmender Anzahl von Kopfzeilen abnimmt).

Kompilieren Sie den EA und starten Sie ihn auf einem Chart:


Wie wir sehen können, funktioniert das Layout der Registerkartenüberschriften auf der linken und rechten Seite korrekt. Es gibt einige Unzulänglichkeiten, die wir im nächsten Artikel beschreiben und beheben werden, aber bis jetzt ist alles gut.


Was kommt als Nächstes?

Im nächsten Artikel werde ich die Arbeit an TabControl fortsetzen.

Alle Dateien der aktuellen Bibliotheksversion, des Test-EA und des Chartereignis-Kontrollindikators für MQL5 sind unten angehängt, damit Sie sie testen und herunterladen können. Schreiben Sie Ihre Fragen, Kommentare und Vorschläge im Kommentarteil.

Zurück zum Inhalt

*Vorherige Artikel in dieser Reihe:

DoEasy. Steuerung (Teil 10): WinForms-Objekte — Animieren der Nutzeroberfläche
DoEasy. Steuerung (Teil 11): WinForms Objekte — Gruppen, das WinForms-Objekt CheckedListBox
DoEasy. Steuerung (Teil 12): Basislistenobjekt, ListBox und ButtonListBox WinForms-Objekte
DoEasy. Steuerung (Teil 13): Optimierung der Interaktion von WinForms-Objekten mit der Maus, Beginn der Entwicklung des WinForms-Objekts TabControl
DoEasy. Steuerung (Teil 14): Neuer Algorithmus zur Benennung von grafischen Elementen. Fortsetzung der Arbeit am TabControl WinForms Objekt
DoEasy. Steuerung (Teil 15): TabControl WinForms Objekt — mehrere Reihen von Registerkartenüberschriften, Methoden zur Behandlung von Registerkarten

Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/11356

Beigefügte Dateien |
MQL5.zip (4435.36 KB)
Neuronale Netze leicht gemacht (Teil 24): Verbesserung des Instruments für Transfer Learning Neuronale Netze leicht gemacht (Teil 24): Verbesserung des Instruments für Transfer Learning
Im vorigen Artikel haben wir ein Tool zum Erstellen und Bearbeiten der Architektur neuronaler Netze entwickelt. Heute werden wir die Arbeit an diesem Instrument fortsetzen. Wir werden versuchen, sie nutzerfreundlicher zu gestalten. Dies mag ein Schritt weg von unserem Thema sein. Aber ist es nicht so, dass ein gut organisierter Arbeitsplatz eine wichtige Rolle bei der Erreichung dieses Ziels spielt?
DoEasy. Steuerung (Teil 15): TabControl WinForms Objekt — mehrere Reihen von Registerkartenüberschriften, Methoden zur Behandlung von Registerkarten DoEasy. Steuerung (Teil 15): TabControl WinForms Objekt — mehrere Reihen von Registerkartenüberschriften, Methoden zur Behandlung von Registerkarten
In diesem Artikel werde ich die Arbeit am Objekt TabControl WinForm fortsetzen — ich werde eine Tabulatorfeld-Objektklasse erstellen, es möglich machen, Tabulatorüberschriften in mehreren Zeilen anzuordnen und Methoden für die Handhabung von Objekttabs hinzufügen.
Einen handelnden Expert Advisor von Grund auf neu entwickeln (Teil 28): Der Zukunft entgegen (III) Einen handelnden Expert Advisor von Grund auf neu entwickeln (Teil 28): Der Zukunft entgegen (III)
Es gibt noch eine Aufgabe, der unser Auftragssystem nicht gewachsen ist, aber wir werden das ENDLICH verstehen. Der MetaTrader 5 bietet ein Ticketsystem, das die Erstellung und Korrektur von Auftragswerten ermöglicht. Die Idee ist, einen Expert Advisor zu haben, der das gleiche Ticketsystem schneller und effizienter machen würde.
Einen technischen Indikator selber machen Einen technischen Indikator selber machen
In diesem Artikel gehe ich auf die Algorithmen ein, mit denen Sie Ihren eigenen technischen Indikator erstellen können. Sie werden lernen, wie man mit sehr einfachen Ausgangsannahmen ziemlich komplexe und interessante Ergebnisse erzielen kann.