NoesisGUI

CustomControl Tutorial

github Tutorial Data

In contrast with UserControls, that are created by composition of controls, a CustomControl extends an existing control. CustomControls can be styled and it is usually the best approach to build a control library.

Creating a CustomControl is quite simple but the challenge is to do it the right way. So before you start creating a control try to answer the following questions:

  • What problem should my control solve?
  • Who will use this control? In which context and environment?
  • Can I extend or compose existing controls? Have you look at existing controls?
  • Should it be possible to style or template my control?
  • Is it used in a single project, or part of a reusable library?

This tutorial gives you a step by step walktrough on how to create a custom control to represent Date and Time.

Choose the right base class

Choosing the right base class is crucial and can save a lot of time. Compare the features of your control with existing controls and start with one that matches close. The following list should give you a good overview from the most lightweight to more heavyweight base types:

  • UIElement: The most lightweight base class to start from. It has support for Layout, Input, Focus and Events.
  • FrameworkElement: Derives from UIElement and adds support for styling, tooltips and context menus. It is the first base class that takes part in the logical tree and so it supports data binding and resource lookup.
  • Control: is the most common base class for controls (its name speaks for itself). It supports templates and adds some basic properties as Foreground, Background and FontSize.
  • ContentControl: is a control that has an additional Content property. This is often used for simple containers.
  • HeaderedContentControl: is a control that has an Content and a Header property. This is used for controls with a header like Expander, TabControl or GroupBox
  • ItemsControl: a control that has an additional Items collection. This is a good choice for controls that display a dynamic list of items without selection.
  • Selector: an ItemsControl whose items can be indexed and selected. This is used for ListBox, ComboBox, ListView or TabControl
  • RangeBase: is the base class for controls that display a value range like Slider or ProgressBar. It adds a Value, Minimum and Maximum property.

In our example we will derive from Control base class because our control requires templates to present date and time information. Other features added by more complex base classes are not required. We will follow the steps described in the tutorial that describes how to extend NoesisGUI.

#include <NsCore/Noesis.h>
#include <NsCore/TypeId.h>
#include <NsCore/ReflectionImplement.h>
#include <NsGui/Control.h>


using namespace Noesis;


namespace CustomControl
{

class DateTime: public Control
{
    NS_IMPLEMENT_INLINE_REFLECTION(DateTime, Control)
    {
        NsMeta<TypeId>("CustomControl.DateTime");
    }
};

}

Override the Default Style

Controls separate behavior from appearance. The behavior is defined in code. The appearance is defined by the XAML. The default template used by the control is by convention wrapped into a style that has an implicit key. We need to override the default value of the DefaultStyleKey property and set it to the Type object of our control.

#include <NsCore/Noesis.h>
#include <NsCore/TypeId.h>
#include <NsCore/ReflectionImplement.h>
#include <NsGui/Control.h>
#include <NsGui/UIElementData.h>
#include <NsGui/ResourceKeyType.h>
#include <NsGui/FrameworkPropertyMetaData.h>


using namespace Noesis;


namespace CustomControl
{

class DateTime: public Control
{
    NS_IMPLEMENT_INLINE_REFLECTION(DateTime, Control)
    {
        NsMeta<TypeId>("CustomControl.DateTime");

        const TypeClass* type = TypeOf<SelfClass>();
        Ptr<ResourceKeyType> defaultStyleKey = ResourceKeyType::Create(type);

        Ptr<UIElementData> data = NsMeta<UIElementData>(type);
        data->OverrideMetadata<Ptr<ResourceKeyType>>(FrameworkElement::DefaultStyleKeyProperty,
            "DefaultStyleKey", FrameworkPropertyMetadata::Create(defaultStyleKey,
            FrameworkOptions_None));
    }
};

}

Properties

Our DateTime control needs some public properties to allow users to modify or present the control. We will add the dependency properties Day, Month and Year for representing the date and Hour, Minute and Second for representing the time.

#include <NsCore/Noesis.h>
#include <NsCore/TypeId.h>
#include <NsCore/ReflectionImplement.h>
#include <NsGui/Control.h>
#include <NsGui/UIElementData.h>
#include <NsGui/ResourceKeyType.h>
#include <NsGui/FrameworkPropertyMetaData.h>


using namespace Noesis;


namespace CustomControl
{

class DateTime: public Control
{
public:
    int GetDay() const
    {
        return GetValue<int>(DayProperty);
    }

    void SetDay(int day)
    {
        SetValue<int>(DayProperty, day);
    }

    int GetMonth() const
    {
        return GetValue<int>(MonthProperty);
    }

    void SetMonth(int month)
    {
        SetValue<int>(MonthProperty, month);
    }

    int GetYear() const
    {
        return GetValue<int>(YearProperty);
    }

    void SetYear(int year)
    {
        SetValue<int>(YearProperty, year);
    }

    int GetHour() const
    {
        return GetValue<int>(HourProperty);
    }

    void SetHour(int hour)
    {
        SetValue<int>(HourProperty, hour);
    }

    int GetMinute() const
    {
        return GetValue<int>(MinuteProperty);
    }

    void SetMinute(int minute)
    {
        SetValue<int>(MinuteProperty, minute);
    }

    int GetSecond() const
    {
        return GetValue<int>(SecondProperty);
    }

    void SetSecond(int second)
    {
        SetValue<int>(SecondProperty, second);
    }

    static const DependencyProperty* DayProperty;
    static const DependencyProperty* MonthProperty;
    static const DependencyProperty* YearProperty;
    static const DependencyProperty* HourProperty;
    static const DependencyProperty* MinuteProperty;
    static const DependencyProperty* SecondProperty;

    NS_IMPLEMENT_INLINE_REFLECTION(DateTime, Control)
    {
        NsMeta<TypeId>("CustomControl.DateTime");

        const TypeClass* type = TypeOf<SelfClass>();
        Ptr<ResourceKeyType> defaultStyleKey = ResourceKeyType::Create(type);

        Ptr<UIElementData> data = NsMeta<UIElementData>(type);
        data->RegisterProperty<int>(DayProperty, "Day",
            FrameworkPropertyMetadata::Create(int(1), FrameworkOptions_None));
        data->RegisterProperty<int>(MonthProperty, "Month",
            FrameworkPropertyMetadata::Create(int(1), FrameworkOptions_None));
        data->RegisterProperty<int>(YearProperty, "Year",
            FrameworkPropertyMetadata::Create(int(2000), FrameworkOptions_None));
        data->RegisterProperty<int>(HourProperty, "Hour",
            FrameworkPropertyMetadata::Create(int(0), FrameworkOptions_None));
        data->RegisterProperty<int>(MinuteProperty, "Minute",
            FrameworkPropertyMetadata::Create(int(0), FrameworkOptions_None));
        data->RegisterProperty<int>(SecondProperty, "Second",
            FrameworkPropertyMetadata::Create(int(0), FrameworkOptions_None));
        data->OverrideMetadata<Ptr<ResourceKeyType>>(FrameworkElement::DefaultStyleKeyProperty,
            "DefaultStyleKey", FrameworkPropertyMetadata::Create(defaultStyleKey,
            FrameworkOptions_None));
    }
};

const DependencyProperty* DateTime::DayProperty;
const DependencyProperty* DateTime::MonthProperty;
const DependencyProperty* DateTime::YearProperty;
const DependencyProperty* DateTime::HourProperty;
const DependencyProperty* DateTime::MinuteProperty;
const DependencyProperty* DateTime::SecondProperty;

}

Templates

Using this control in any application will require that you specify a template for the DateTime type. We are going to provide two different approaches to demonstrate the power of control styling and templating.

The first template shows the date and time as a digital clock:

CustomControlTutorialImg1.jpg
<!-- Digital clock style -->
<ControlTemplate x:Key="DigitalDateTimeTemplate" TargetType="{x:Type local:DateTime}">
  <Viewbox>
    <StackPanel>
      <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" TextElement.FontSize="40">
        <TextBlock Text="{Binding Hour, StringFormat=G, RelativeSource={RelativeSource TemplatedParent}}"/>
        <TextBlock Text=":"/>
        <TextBlock Text="{Binding Minute, StringFormat=G, RelativeSource={RelativeSource TemplatedParent}}"/>
      </StackPanel>
      <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" TextElement.FontSize="16">
        <TextBlock Text="{Binding Day, StringFormat=G, RelativeSource={RelativeSource TemplatedParent}}"/>
        <TextBlock Text="/"/>
        <TextBlock Text="{Binding Month, StringFormat=G, RelativeSource={RelativeSource TemplatedParent}}"/>
        <TextBlock Text="/"/>
        <TextBlock Text="{Binding Year, StringFormat=G, RelativeSource={RelativeSource TemplatedParent}}"/>
      </StackPanel>
    </StackPanel>
  </Viewbox>
</ControlTemplate>

<Style x:Key="DigitalStyle">
  <Setter Property="local:DateTime.Template" Value="{StaticResource DigitalDateTimeTemplate}"/>
</Style>

The second template is an attempt to simulate an analog clock:

CustomControlTutorialImg2.jpg
<!-- Converters needed -->
<local:HoursConverter x:Key="DateTimeHoursConverter"/>
<local:MinutesConverter x:Key="DateTimeMinutesConverter"/>
<local:SecondsConverter x:Key="DateTimeSecondsConverter"/>

<!-- Analog clock style -->
<ControlTemplate x:Key="AnalogDateTimeTemplate" TargetType="{x:Type local:DateTime}">
  <Viewbox>
    <Grid Height="200" Width="200">
      <Ellipse StrokeThickness="6" Stretch="Uniform">
        <Ellipse.Stroke>
          <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
            <GradientStop Color="#FF4D4D4D" Offset="1"/>
            <GradientStop Color="Gray"/>
          </LinearGradientBrush>
        </Ellipse.Stroke>
      </Ellipse>
      <Ellipse StrokeThickness="5" Stretch="Uniform" Margin="5" Fill="White">
        <Ellipse.Stroke>
          <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
            <GradientStop Color="#FF333333" Offset="0"/>
            <GradientStop Color="#FF999999" Offset="1"/>
          </LinearGradientBrush>
        </Ellipse.Stroke>
      </Ellipse>

      <Grid Margin="18" TextElement.FontSize="24">
        <TextBlock Text="12" HorizontalAlignment="Center" VerticalAlignment="Top"/>
        <TextBlock Text="3" HorizontalAlignment="Right" VerticalAlignment="Center"/>
        <TextBlock Text="6" HorizontalAlignment="Center" VerticalAlignment="Bottom"/>
        <TextBlock Text="9" HorizontalAlignment="Left" VerticalAlignment="Center"/>
      </Grid>

      <Path x:Name="BarS" Data="M100,100L100,20" Stroke="#FF333333" StrokeThickness="2"
                       RenderTransformOrigin="0.5,0.5">
        <Path.RenderTransform>
          <TransformGroup>
            <ScaleTransform/>
            <SkewTransform/>
            <RotateTransform Angle="{Binding Second,
                                   Converter={StaticResource DateTimeSecondsConverter},
                                   RelativeSource={RelativeSource TemplatedParent}}"/>
            <TranslateTransform/>
          </TransformGroup>
        </Path.RenderTransform>
      </Path>
      <Path x:Name="BarM" Data="M100,100L100,20" Stroke="Red" StrokeThickness="4"
                       RenderTransformOrigin="0.5,0.5">
        <Path.RenderTransform>
          <TransformGroup>
            <ScaleTransform/>
            <SkewTransform/>
            <RotateTransform Angle="{Binding Minute,
                                   Converter={StaticResource DateTimeMinutesConverter},
                                   RelativeSource={RelativeSource TemplatedParent}}"/>
            <TranslateTransform/>
          </TransformGroup>
        </Path.RenderTransform>
      </Path>
      <Path x:Name="BarH" Data="M100,100L100,40" Stroke="#FFCA0000" StrokeThickness="8"
                       RenderTransformOrigin="0.5,0.5">
        <Path.RenderTransform>
          <TransformGroup>
            <ScaleTransform/>
            <SkewTransform/>
            <RotateTransform Angle="{Binding Hour,
                                   Converter={StaticResource DateTimeHoursConverter},
                                   RelativeSource={RelativeSource TemplatedParent}}"/>
            <TranslateTransform/>
          </TransformGroup>
        </Path.RenderTransform>
      </Path>

      <Ellipse HorizontalAlignment="Center" VerticalAlignment="Center" Width="20" Height="20">
        <Ellipse.Fill>
          <RadialGradientBrush>
            <GradientStop Color="#FF343434" Offset="0.2"/>
            <GradientStop Color="#FF666666" Offset="1"/>
            <GradientStop Color="Gray" Offset="0.95"/>
            <GradientStop Color="#FF999999"/>
            <GradientStop Color="#FF404040" Offset="0.9"/>
          </RadialGradientBrush>
        </Ellipse.Fill>
      </Ellipse>
    </Grid>
  </Viewbox>
</ControlTemplate>

<Style TargetType="{x:Type local:DateTime}">
  <Setter Property="Template" Value="{StaticResource AnalogDateTimeTemplate}"/>
</Style>

The second style needs a few converters to transform control properties into the appropriate rotation angles.

class HoursConverter: public BaseValueConverter
{
public:
    bool TryConvert(BaseComponent* value, const Type* type, BaseComponent* /*parameter*/,
        Ptr<BaseComponent>& result)
    {
        if (Boxing::CanUnbox<int>(value) && type == TypeOf<float>())
        {
            int hours = Boxing::Unbox<int>(value);
            result = Boxing::Box(hours * 30.0f);
            return true;
        }

        return false;
    }

    NS_IMPLEMENT_INLINE_REFLECTION(HoursConverter, BaseValueConverter)
    {
        NsMeta<TypeId>("CustomControl.HoursConverter");
    }
};

class MinutesConverter: public BaseValueConverter
{
public:
    bool TryConvert(BaseComponent* value, const Type* type, BaseComponent* /*parameter*/,
        Ptr<BaseComponent>& result)
    {
        if (Boxing::CanUnbox<int>(value) && type == TypeOf<float>())
        {
            int minutes = Boxing::Unbox<int>(value);
            result = Boxing::Box(minutes * 6.0f);
            return true;
        }

        return false;
    }

    NS_IMPLEMENT_INLINE_REFLECTION(MinutesConverter, BaseValueConverter)
    {
        NsMeta<TypeId>("CustomControl.MinutesConverter");
    }
};

class SecondsConverter: public BaseValueConverter
{
public:
    bool TryConvert(BaseComponent* value, const Type* type, BaseComponent* /*parameter*/,
        Ptr<BaseComponent>& result)
    {
        if (Boxing::CanUnbox<int>(value) && type == TypeOf<float>())
        {
            int seconds = Boxing::Unbox<int>(value);
            result = Boxing::Box(seconds * 6.0f);
            return true;
        }

        return false;
    }

    NS_IMPLEMENT_INLINE_REFLECTION(SecondsConverter, BaseValueConverter)
    {
        NsMeta<TypeId>("CustomControl.SecondsConverter");
    }
};

Improvements

A cool feature for this control could be automatic updating from system time and date. We can achieve this with a timer in the control instance that updates the time and date values whenever timer is ticked. Then we can add a boolean property to allow this feature to be enabled or disabled from within XAML.

© 2017 Noesis Technologies