Files
GacUI/.github/KnowledgeBase/manual/gacui/kb/ctemplates.md
T
2026-02-04 14:57:24 -08:00

11 KiB

Control Templates

Control templates change controls' outfit, and they are working based on data bindings.

Controls have their own control template types. For example,SelectableButtonneedsSelectableButtonTemplate. Any derived classes ofSelectableButtonTemplateis acceptable, but extra members will be ignored.

A control template has input properties and output properties. Input properties are for data binding, when the control's state is changed, these properties will be changed, and the control template reacts to these properties and changes how it looks, for example, some colors. Output properties are configuration to the control's outfit related behavior, almost all of them will only be read once, so changing them at runtime takes no effect.

Here we demo how to write a control template forSelectableButton. When the mouse is hovering on the button, it becomes brighter. When the button is selected, it becomes less brighter.

These are properties inControlTemplate:

  • Input Properties:
    • Focused: True when the control is focused.
    • VisuallyEnabled: Trhe when the control is enabled. It requires all parent controls to be enabled.
    • Font: The control's font. If a font is not associated to the control, it will be the font from a parent control.
    • Context: The control'sContextproperty.
    • Text: The control'sTextproperty.
  • Output Properties:
    • ContainerComposition: A composition for control's children, anything put in <SelectableButton></SelectableButton> will be in this composition.
    • FocusableComposition: A composition for receiving keyboard events. It works only when the control is focusable.

These are properties inButtonTemplateandSelectableButtonTemplate:

  • Input Properties:
    • State: The button's state, can beNormal,ActiveorPressed.
    • Selected: True when the button is selected.

We can use all of them to create a control template forSelectableButton:

<Instance ref.Class="sample::TabHeaderButtonTemplate">
  <SelectableButtonTemplate ref.Name="self" MinSizeLimitation="LimitToElementAndChildren">
    <Bounds AlignmentToParent="left:0 top:0 right:0 bottom:0">
      <SolidBackground>
        <att.Color-bind><![CDATA[
          cast Color (
            not self.VisuallyEnabled ? "#00000000" :
            self.State == ButtonState::Pressed ? "#3D3D3D" :
            self.State == ButtonState::Active ? "#808080" :
            self.Selected ? "#3D3D3D" :
            "#00000000"
          )
        ]]></att.Color-bind>
      </SolidBackground>
    </Bounds>
    <Bounds AlignmentToParent="left:0 top:0 right:0 bottom:0">
      <att.Visible-bind>self.Focused</att.Visible-bind>
      <FocusRectangle/>
    </Bounds>
    <Bounds AlignmentToParent="left:1 top:1 right:1 bottom:1" MinSizeLimitation="LimitToElement">
      <SolidLabel Text-bind="self.Text" Font-bind="self.Font">
        <att.Color-bind><![CDATA[
          cast Color (
            self.VisuallyEnabled ? "#FFFFFF" : "808080"
          )
        ]]></att.Color-bind>
      </SolidLabel>
    </Bounds>
  </SelectableButtonTemplate>
</Instance>

And then a window containing three check boxes with this control template:

<Instance ref.Class="sample::MainWindow" ref.Styles="res://MainWindow/SharedStyle">
  <Window Text="kb_ctemplates" IconVisible="true" ClientSize="x:640 y:480">
    <att.BoundsComposition-set PreferredMinSize="x:480 y:320"/>
      <Stack Direction="Vertical" MinSizeLimitation="LimitToElementAndChildren" AlignmentToParent="left:5 top:5 right:-1 bottom:-1" Padding="5">
        <StackItem><CheckBox Text="Button 1"/></StackItem>
        <StackItem><CheckBox Text="Button 2"/></StackItem>
        <StackItem><CheckBox Text="Button 3"/></StackItem>
      </Stack>
  </Window>
</Instance>

With a style to assign control templates to all check boxes:

<Styles>
  <Style ref.Path="//CheckBox">
    <att.ControlTemplate>sample::TabHeaderButtonTemplate</att.ControlTemplate>
    <att.BoundsComposition-set AlignmentToParent="left:0 top:0 right:0 bottom:0"/>
  </Style>
</Styles>

Finally we get:

With this new control template, we can create a more complex control template for tab control.

TabneedsTabTemplate, and it has two important properties:CommandsandTabPages. These are all input properties.Commandsis an interface with a methodShowTabfor activate a tab page.TabPagesis an observable collection for all tab pages at runtime.

By bindingTabPagesto aRepeatStack, we can easily create a list of buttons for each tab pages. When tab pages are inserted to or removed from the tab control, the list of buttons will be automatically refreshed.

Firstly we create a control template forTab:

<Instance ref.Class="sample::TabTemplate">
  <TabTemplate ref.Name="self" ContainerComposition-ref="container" TabOrder="TopToBottom" MinSizeLimitation="LimitToElementAndChildren">
    <Table AlignmentToParent="left:0 top:0 right:0 bottom:0" MinSizeLimitation="LimitToElementAndChildren">
      <att.Rows>
        <CellOption>composeType:Percentage percentage:1.0</CellOption>
      </att.Rows>
      <att.Columns>
        <CellOption>composeType:MinSize</CellOption>
        <CellOption>composeType:Percentage percentage:1.0</CellOption>
      </att.Columns>

      <Cell Site="row:0 column:0">
        <RepeatStack ref.Name="header" AlignmentToParent="left:0 top:0 right:0 bottom:0" Padding="2" Direction="Vertical" MinSizeLimitation="LimitToElementAndChildren">
          <att.ItemTemplate>sample::TabHeaderTemplate</att.ItemTemplate>
          <att.ItemSource-bind>self.TabPages</att.ItemSource-bind>
          <ev.ItemInserted-eval>
            <![CDATA[
              {
                var stackItem = header.StackItems[arguments.itemIndex];
                var item = cast (TabHeaderTemplate*) stackItem.Children[0];
                item.Commands = self.Commands;
              }
            ]]>
          </ev.ItemInserted-eval>
        </RepeatStack>
      </Cell>

      <Cell Site="row:0 column:1">
        <SolidBackground Color="#3D3D3D"/>
        <Bounds ref.Name="container" AlignmentToParent="left:1 top:1 right:1 bottom:1"/>
      </Cell>
    </Table>
  </TabTemplate>
</Instance>

In this control template, we create a list of buttons usingRepeatStackon the left side, and create a container for tab page contents on the right side.

The item template inRepeatStackis namedsample::TabHeaderTemplate, which is:

<Instance ref.Class="sample::TabHeaderTemplate">
  <ref.Parameter Name="CurrentTabPage" Class="presentation::controls::GuiTabPage"/>
  <ref.Members>
    <![CDATA[
      prop Commands : ITabCommandExecutor* = null {}
    ]]>
  </ref.Members>
  <ControlTemplate ref.Name="self" MinSizeLimitation="LimitToElementAndChildren">
    <CheckBox AutoFocus="false" AutoSelection="false">
      <att.ControlTemplate>sample::TabHeaderButtonTemplate</att.ControlTemplate>
      <att.BoundsComposition-set AlignmentToParent="left:0 top:0 right:0 bottom:0"/>
      <att.Context-ref>CurrentTabPage</att.Context-ref>
      <att.Text-bind>CurrentTabPage.Text</att.Text-bind>
      <att.Alt-bind>CurrentTabPage.Alt</att.Alt-bind>
      <att.Selected-bind>CurrentTabPage.OwnerTab.SelectedPage == CurrentTabPage</att.Selected-bind>
      <ev.Clicked-eval>
        <![CDATA[
          {
            self.Commands.ShowTab(CurrentTabPage.OwnerTab.Pages.IndexOf(CurrentTabPage), true);
          }
        ]]>
      </ev.Clicked-eval>
    </CheckBox>
  </ControlTemplate>
</Instance>

sample::TabHeaderTemplatehas aCommandsproperty, it needs to be assigned insample::TabTemplate, so that when a tab header is clicked, it can tell the tab control to switch to that tab.

sample::TabTemplatereacts toItemInsertedevent inRepeatStack, by accessingarguments.itemIndexwhich is provided by the event, it knows a new item template is created, and grab that item template to set theCommandsproperty.

This ensures that whenever a new item template for the tab header appears in the tab control, theCommandsproperty has already been assigned.

InRepeatStackthere is**<att.ItemSource-bind>self.TabPages</att.ItemSource-bind>. This code bindsTabPagestoRepeatStack**. And**<att.ItemTemplate>sample::TabHeaderTemplate</att.ItemTemplate>tells it to createsample::TabHeaderTemplate**for each tab page.

The assigned tab page for a particularsample::TabHeaderTemplatewill be inref.Parameter, which is a readonly property and constructor argument.

Knowledge base for item templates will be introduces in the next chapter.

Finally, we create a window with three tab pages to see how it works:

<Folder name="MainWindow">
  <InstanceStyle name="SharedStyle">
    <Styles>
      <Style ref.Path="//Stack" Direction="Vertical" MinSizeLimitation="LimitToElementAndChildren"/>
      <Style ref.Path="//RadioButton" GroupController-ref="groupDirection"/>
      <Style ref.Path="//TabPage">
        <att.ContainerComposition-set InternalMargin="left:10 top:10 right:10 bottom:10"/>
      </Style>
    </Styles>
  </InstanceStyle>

  <Instance name="MainWindowResource">
    <Instance ref.Class="sample::MainWindow" ref.Styles="res://MainWindow/SharedStyle" xmlns:x="presentation::controls::GuiSelectableButton::*">
      <Window Text="kb_ctemplates" IconVisible="true" ClientSize="x:640 y:480">
        <att.BoundsComposition-set PreferredMinSize="x:480 y:320"/>
        <x:MutexGroupController ref.Name="groupDirection"/>

        <Tab>
          <att.ControlTemplate>sample::TabTemplate</att.ControlTemplate>
          <att.BoundsComposition-set AlignmentToParent="left:0 top:0 right:0 bottom:0"/>
          <att.Pages>
            <TabPage Text="First TabPage">
              <Button Text="A button in the first page"/>
            </TabPage>

            <TabPage Text="Second TabPage">
              <Stack>
                <StackItem><CheckBox Text="A check box in the second page"/></StackItem>
                <StackItem><CheckBox Text="A check box in the second page"/></StackItem>
                <StackItem><CheckBox Text="A check box in the second page"/></StackItem>
              </Stack>
            </TabPage>

            <TabPage Text="Third TabPage">
              <Stack>
                <StackItem><RadioButton Text="A radio button in the third page"/></StackItem>
                <StackItem><RadioButton Text="A radio button in the third page"/></StackItem>
                <StackItem><RadioButton Text="A radio button in the third page"/></StackItem>
              </Stack>
            </TabPage>
          </att.Pages>
        </Tab>
      </Window>
    </Instance>
  </Instance>
</Folder>

Here we have a button for the first tab page, three check boxes for the second tab page, and three radio buttons for the third tab page. Each radio button is assigned the same group controller so that they know they are exclusive to each other. By assigning different group controllers to radio buttons, it is very easy to tell these radio buttons who are their allies.

Finally we get: