NoesisGUI

CustomControl Tutorial

zip Tutorial Data

In contrast with UserControl, that is 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 or 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, 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, TabControl...
  • RangeBase: is the base class for controls that display a value range like Sliders or ProgressBars. 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 info. Other features added by more complex base classes are not required. We must follow the steps described in the tutorial that describes how to extend NoesisGUI.

#pragma warning(disable: 4275)
#pragma warning(disable: 4251)


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


using namespace Noesis;
using namespace Noesis::Core;
using namespace Noesis::Gui;


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

////////////////////////////////////////////////////////////////////////////////////////////////////
extern "C" NS_DLL_EXPORT
void NsRegisterReflection(ComponentFactory* factory, NsBool registerComponents)
{
    NS_REGISTER_COMPONENT(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.

#pragma warning(disable: 4275)
#pragma warning(disable: 4251)


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


using namespace Noesis;
using namespace Noesis::Core;
using namespace Noesis::Gui;


////////////////////////////////////////////////////////////////////////////////////////////////////
class DateTime: public Control
{
    NS_IMPLEMENT_INLINE_REFLECTION(DateTime, Control)
    {
        NsMeta<TypeId>("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));
    }
};

////////////////////////////////////////////////////////////////////////////////////////////////////
extern "C" NS_DLL_EXPORT
void NsRegisterReflection(ComponentFactory* factory, NsBool registerComponents)
{
    NS_REGISTER_COMPONENT(DateTime)
}

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.

#pragma warning(disable: 4275)
#pragma warning(disable: 4251)


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


using namespace Noesis;
using namespace Noesis::Core;
using namespace Noesis::Gui;


////////////////////////////////////////////////////////////////////////////////////////////////////
class DateTime: public Control
{
public:
    /// Gets or sets day
    //@{
    NsInt32 GetDay() const
    {
        return GetValue<NsInt32>(DayProperty);
    }
    void SetDay(NsInt32 day)
    {
        SetValue<NsInt32>(DayProperty, day);
    }
    //@}

    /// Gets or sets month
    //@{
    NsInt32 GetMonth() const
    {
        return GetValue<NsInt32>(MonthProperty);
    }
    void SetMonth(NsInt32 month)
    {
        SetValue<NsInt32>(MonthProperty, month);
    }
    //@}

    /// Gets or sets year
    //@{
    NsInt32 GetYear() const
    {
        return GetValue<NsInt32>(YearProperty);
    }
    void SetYear(NsInt32 year)
    {
        SetValue<NsInt32>(YearProperty, year);
    }
    //@}

    /// Gets or sets hour
    //@{
    NsInt32 GetHour() const
    {
        return GetValue<NsInt32>(HourProperty);
    }
    void SetHour(NsInt32 hour)
    {
        SetValue<NsInt32>(HourProperty, hour);
    }
    //@}

    /// Gets or sets minute
    //@{
    NsInt32 GetMinute() const
    {
        return GetValue<NsInt32>(MinuteProperty);
    }
    void SetMinute(NsInt32 minute)
    {
        SetValue<NsInt32>(MinuteProperty, minute);
    }
    //@}

    /// Gets or sets second
    //@{
    NsInt32 GetSecond() const
    {
        return GetValue<NsInt32>(SecondProperty);
    }
    void SetSecond(NsInt32 second)
    {
        SetValue<NsInt32>(SecondProperty, second);
    }
    //@}

public:
    static const Gui::DependencyProperty* DayProperty;
    static const Gui::DependencyProperty* MonthProperty;
    static const Gui::DependencyProperty* YearProperty;
    static const Gui::DependencyProperty* HourProperty;
    static const Gui::DependencyProperty* MinuteProperty;
    static const Gui::DependencyProperty* SecondProperty;

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

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

        Ptr<UIElementData> data = NsMeta<UIElementData>(type);
        data->RegisterProperty<NsInt32>(DayProperty, "Day",
            FrameworkPropertyMetadata::Create(NsInt32(1), FrameworkOptions_None));
        data->RegisterProperty<NsInt32>(MonthProperty, "Month",
            FrameworkPropertyMetadata::Create(NsInt32(1), FrameworkOptions_None));
        data->RegisterProperty<NsInt32>(YearProperty, "Year",
            FrameworkPropertyMetadata::Create(NsInt32(2000), FrameworkOptions_None));
        data->RegisterProperty<NsInt32>(HourProperty, "Hour",
            FrameworkPropertyMetadata::Create(NsInt32(0), FrameworkOptions_None));
        data->RegisterProperty<NsInt32>(MinuteProperty, "Minute",
            FrameworkPropertyMetadata::Create(NsInt32(0), FrameworkOptions_None));
        data->RegisterProperty<NsInt32>(SecondProperty, "Second",
            FrameworkPropertyMetadata::Create(NsInt32(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;

////////////////////////////////////////////////////////////////////////////////////////////////////
extern "C" NS_DLL_EXPORT
void NsRegisterReflection(ComponentFactory* factory, NsBool registerComponents)
{
    NS_REGISTER_COMPONENT(DateTime)
}

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 normal text string:

CustomControlTutorialImg1.jpg
<Grid
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid.Resources>
        <ControlTemplate x:Key="TextDateTimeTemplate" TargetType="{x:Type DateTime}">
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
                <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}}"/>
                <TextBlock Text="{Binding Hour, StringFormat=G, RelativeSource={RelativeSource TemplatedParent}}" Margin="8,0,0,0"/>
                <TextBlock Text=":"/>
                <TextBlock Text="{Binding Minute, StringFormat=G, RelativeSource={RelativeSource TemplatedParent}}"/>
                <TextBlock Text=":"/>
                <TextBlock Text="{Binding Second, StringFormat=G, RelativeSource={RelativeSource TemplatedParent}}"/>
            </StackPanel>
        </ControlTemplate>
        <Style TargetType="{x:Type DateTime}">
            <Setter Property="Template" Value="{StaticResource TextDateTimeTemplate}"/>
        </Style>
    </Grid.Resources>
    <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
        <TextBlock Text="Date &amp; Time:" FontSize="16" Foreground="DodgerBlue" VerticalAlignment="Center"/>
        <Border BorderBrush="Black" BorderThickness="1" Padding="4,1" Margin="10,0">
            <DateTime Day="6" Month="2" Year="2012" Hour="18" Minute="32" Second="46" VerticalAlignment="Center"/>
        </Border>
    </StackPanel>
</Grid>

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

CustomControlTutorialImg2.jpg
<Grid
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
     <Grid.Resources>
         <HoursConverter x:Key="DateTimeHoursConverter"/>
         <MinutesConverter x:Key="DateTimeMinutesConverter"/>
         <SecondsConverter x:Key="DateTimeSecondsConverter"/>
         <ControlTemplate x:Key="AnalogDateTimeTemplate" TargetType="{x:Type DateTime}">
             <Viewbox Stretch="Uniform">
                 <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 DateTime}">
             <Setter Property="Template" Value="{StaticResource AnalogDateTimeTemplate}"/>
         </Style>
     </Grid.Resources>
     <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
         <Border BorderBrush="Black" BorderThickness="1" Padding="4,1" Margin="10,0">
             <DateTime Day="6" Month="2" Year="2012" Hour="19" Minute="12" Second="51" VerticalAlignment="Center"/>
         </Border>
     </StackPanel>
 </Grid>

NOTE: As you can see in the XAML code, we used converters to transform control property values into the appropriate rotation angles. This converters will be components of your application that implement the IValueConverter interface. Take a look on how simple is to implement them:

#pragma warning(disable: 4275)
#pragma warning(disable: 4251)


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


using namespace Noesis;
using namespace Noesis::Core;
using namespace Noesis::Core::Boxing;
using namespace Noesis::Gui;


////////////////////////////////////////////////////////////////////////////////////////////////////
class DateTime: public Control
{
public:
    /// Gets or sets day
    //@{
    NsInt32 GetDay() const
    {
        return GetValue<NsInt32>(DayProperty);
    }
    void SetDay(NsInt32 day)
    {
        SetValue<NsInt32>(DayProperty, day);
    }
    //@}

    /// Gets or sets month
    //@{
    NsInt32 GetMonth() const
    {
        return GetValue<NsInt32>(MonthProperty);
    }
    void SetMonth(NsInt32 month)
    {
        SetValue<NsInt32>(MonthProperty, month);
    }
    //@}

    /// Gets or sets year
    //@{
    NsInt32 GetYear() const
    {
        return GetValue<NsInt32>(YearProperty);
    }
    void SetYear(NsInt32 year)
    {
        SetValue<NsInt32>(YearProperty, year);
    }
    //@}

    /// Gets or sets hour
    //@{
    NsInt32 GetHour() const
    {
        return GetValue<NsInt32>(HourProperty);
    }
    void SetHour(NsInt32 hour)
    {
        SetValue<NsInt32>(HourProperty, hour);
    }
    //@}

    /// Gets or sets minute
    //@{
    NsInt32 GetMinute() const
    {
        return GetValue<NsInt32>(MinuteProperty);
    }
    void SetMinute(NsInt32 minute)
    {
        SetValue<NsInt32>(MinuteProperty, minute);
    }
    //@}

    /// Gets or sets second
    //@{
    NsInt32 GetSecond() const
    {
        return GetValue<NsInt32>(SecondProperty);
    }
    void SetSecond(NsInt32 second)
    {
        SetValue<NsInt32>(SecondProperty, second);
    }
    //@}

public:
    static const Gui::DependencyProperty* DayProperty;
    static const Gui::DependencyProperty* MonthProperty;
    static const Gui::DependencyProperty* YearProperty;
    static const Gui::DependencyProperty* HourProperty;
    static const Gui::DependencyProperty* MinuteProperty;
    static const Gui::DependencyProperty* SecondProperty;

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

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

        Ptr<UIElementData> data = NsMeta<UIElementData>(type);
        data->RegisterProperty<NsInt32>(DayProperty, "Day",
            FrameworkPropertyMetadata::Create(NsInt32(1), FrameworkOptions_None));
        data->RegisterProperty<NsInt32>(MonthProperty, "Month",
            FrameworkPropertyMetadata::Create(NsInt32(1), FrameworkOptions_None));
        data->RegisterProperty<NsInt32>(YearProperty, "Year",
            FrameworkPropertyMetadata::Create(NsInt32(2000), FrameworkOptions_None));
        data->RegisterProperty<NsInt32>(HourProperty, "Hour",
            FrameworkPropertyMetadata::Create(NsInt32(0), FrameworkOptions_None));
        data->RegisterProperty<NsInt32>(MinuteProperty, "Minute",
            FrameworkPropertyMetadata::Create(NsInt32(0), FrameworkOptions_None));
        data->RegisterProperty<NsInt32>(SecondProperty, "Second",
            FrameworkPropertyMetadata::Create(NsInt32(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;

////////////////////////////////////////////////////////////////////////////////////////////////////
class HoursConverter: public BaseValueConverter
{
public:
    NsBool TryConvert(BaseComponent* value, const Type* targetType, BaseComponent* parameter,
        Ptr<Core::BaseComponent>& result)
    {
        NS_ASSERT(targetType == TypeOf<NsFloat32>());
        NsInt32 hour = Unbox<NsInt32>(NsStaticCast<BoxedValue*>(value));
        result = Boxing::Box<NsFloat32>(hour * 30.0f);
        return true;
    }

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

////////////////////////////////////////////////////////////////////////////////////////////////////
class MinutesConverter: public BaseValueConverter
{
public:
    NsBool TryConvert(BaseComponent* value, const Type* targetType, BaseComponent* parameter,
        Ptr<Core::BaseComponent>& result)
    {
        NS_ASSERT(targetType == TypeOf<NsFloat32>());
        NsInt32 hour = Unbox<NsInt32>(NsStaticCast<BoxedValue*>(value));
        result = Boxing::Box<NsFloat32>(hour * 6.0f);
        return true;
    }

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

////////////////////////////////////////////////////////////////////////////////////////////////////
class SecondsConverter: public BaseValueConverter
{
public:
    NsBool TryConvert(BaseComponent* value, const Type* targetType, BaseComponent* parameter,
        Ptr<Core::BaseComponent>& result)
    {
        NS_ASSERT(targetType == TypeOf<NsFloat32>());
        NsInt32 hour = Unbox<NsInt32>(NsStaticCast<BoxedValue*>(value));
        result = Boxing::Box<NsFloat32>(hour * 6.0f);
        return true;
    }

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

////////////////////////////////////////////////////////////////////////////////////////////////////
extern "C" NS_DLL_EXPORT void NsRegisterReflection(ComponentFactory* factory,
    NsBool registerComponents)
{
    NS_REGISTER_COMPONENT(DateTime)
    NS_REGISTER_COMPONENT(HoursConverter)
    NS_REGISTER_COMPONENT(MinutesConverter)
    NS_REGISTER_COMPONENT(SecondsConverter)
}

Improvements

A cool feature for this control could be to auto update with system time and date. We can achieve this with a timer in the control instance that updates the time and date values when 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