This tutorial is a first one in the tutorial cycle dedicated to packages and their usage in Form Builder. Here we describe entire project goal and create custom widget to be used in further tutorials. We suppose you already familiar with creating forms and applications with Nitisa and won't describe basic steps you need to perform as it was done in Basic and Medium level tutorials. Instead we will focus on new information and key moments only. In this tutorial you will learn how to create your own custom widgets, how to use built-in controls and modify default behaviour of controls.
You are already know how it's easy to create forms for applications in the Form Builder. You just pick needed widget, put it where you need it on a form and so on. But you also might notice that a set of widgets is limited and you might want to create your own widgets needed for your projects as well as you might want your widgets are also appear in the Form Builder and be properly managed by it. In this multipart tutorial we will cover entire process from creating a widget till adding it to the Form Builder.
The first part (this one) of the series is dedicated to creating a custom widget. In the second part we will show how to add widget into Form Builder. And in the last part you will learn how to make Form Builder properly work with custom properties your custom widgets may have.
In this tutorial we are going to create Registration widget. This widget will be something like a small registration form where user can type his first and last name and also select his date of birth. Such a functionality can easy be added to any application by putting only 6 widgets (3 Label widgets to label input fields meaning, 2 Edit widgets for typing first and last names, and DatePicker widget to select date of birth) to a form. But if you create such forms often you need each time do that, each time move widgets to proper positions and do other staff. That might be a little inconvenient and you might want to do all that staff in couple of clicks. So, such set of widgets can be a good candidate to be made a separate widget.
It might seem hard to create such a complicated widget as it has besides labels, which are just texts drawn on a widget and easy implementable, such parts as inputs for texts and even a calendar. Fortunately there is a built-in controls which already implement required functionality and thus using them will extremely simplify our work.
Custom widgets can be added only to a standalone Form Builder. So in this tutorial we will work only with Nitisa downloaded as source code which you can do at this page. It's Okay to have both extension and source code at the same time, so, if you also has Nitisa extension for Visual Studio installed, you don't need to uninstall it. Just download the latest release and unpack it somewhere. Lets assume you unpacked it into C: drive, so the Nitisa source code is in the C:\Nitisa-10.2.0 directory (the version part may be different of course).
When you create something which can be used many times in different projects it's a good idea to put it into a separate library. In the Nitisa such a library is called a Package. You put into a package widgets, forms and other staff you are going to use in multiple projects. The package even can be sent to another developers so they could use it as well. You also can redistribute your package(s) via our portal as described in Distributing Packages article.
Package is meant to be used many times in different projects and thus it should be thoroughly tested and cleaned from bugs but it's a library and it means we need some application to facilitate running library code. On the other hand, it's almost definitely will be used in the Form Builder. And, if it is managed by the Form Builder it can be easy to create an application having widgets from the package on forms and thus it can be easy to create such an application for better testing. But before creating additional applications for testing package code it would be really helpful to be able to debug library when it is managed by the Form Builder itself. So, how can we do that? You know that Nitisa source code has Nitisa.sln solution file for Visual Studio. If you open it, you will see all the Nitisa projects inside. They are packages for different platforms, tutorials source code and also Form Builder code as well. So, we can set FormBuilder project as start-up project and debug it in Visual Studio.
Form Builder loads packages and it will load ours, so we can add our new package and related projects to the Nitisa solution and thus be able to debug it easily using FormBuilder application. That is why we will create all projects directly in the Nitisa solution. Later, when you finish with your package, you can simply delete all previously added projects.
So, lets create a library for our first custom widget in the Nitisa solution.
As you might recall Form Builder is available in four configurations (Debug, DebugRT, Release and ReleaseRT) and two platforms (x86 and x64). We, for simplicity, will use only DebugRT configuration and x86 platform. When you create a real package you are going to share with another developers, you will also definitely use x64 platform and ReleaseRT configuration. As for Debug and Release configurations you will need it only if you want us to include your package into Nitisa extension for Visual Studio. So, lets setup our package.
#include "stdafx.h"
at the beginning of the stdafx.cpp file.It may look quite complicated but all we've done here is just copied settings from, lets say, Standard project in the solution. All packages in the solution have the same options. Yours should also have them. The only difference is in paths. That is because of we put the project into Tutorials folder. If you create a real package, you will place it into Packages directory and all the paths will be just like in another packages.
Package may have a lot of files and user who will use it may be interested to include everything from a package with one include line only. It is a good idea to have a header file for that. You might object we already have stdafx.h file which can be used for that but it's now quite true. The stdafx.h file should have all includes required by the package. It may not only be package header files but also any others like header files of Standard Template Library and others. So lets create a separate file which will contain includes of all the package header files.
#include "CoolWidgets.h"
to the stdafx.h file.#include "Nitisa/Nitisa.h"
to the stdafx.h file as well.You may now select DebugRT configuration and x86 platform and build the project. If everything is done right, you will see CoolWidgets.lib library file in the C:\Nitisa-10.2.0\bin\Windows\x86\DebugRT directory.
By the next step we will create a simple application project we will use to debug our widget later. We need it in development a widget as we will add possibility to manage widget in the Form Builder only in the next tutorial.
When you have several projects depending on each other in the solution it is a good idea to set dependencies so that projects are re-built automatic when it is needed by Visual Studio. Our just added application depends on the package project we added earlier and it will also depend on Nitisa project (framework core), Standard package as it is needed for the next one, and it depends on Platform project as well (here are all platform-dependent classes, like window, renderer, application, leaves). So click on the project name by right mouse button and in the popup menu select Build Dependencies -> Project Dependencies and check those four projects there. There should be several projects with the same names. You need the once marked as Windows like shown below.
Package project CoolWidgets needn't to depend on anything. Only applications and dynamic libraries require dependencies.
To finish with the test application project lets add an empty form and application initialization into main.cpp
file.
Add new FormMain.h
header file and put following code into it.
#pragma once
#include "Nitisa/Core/Form.h"
namespace nitisa
{
class CFormMain :public CForm
{
public:
CFormMain();
};
extern CFormMain *FormMain;
}
This is a pretty simple form declaration. It is very similar to what you did before when created applications with Nitisa. There is only two small differences. The first one is that we omit nested namespace and put form class declaration directly into nitisa namespace. We did this for simplicity. And the next difference is that our form class is derived directly from CForm class instead of a form prototype class as usually. Our form is empty and thus we need no additional initialization performed in form prototype class generated by the Form Builder. So we do not use Form Builder and derive from directly form the default form implementation class.
Add new FormMain.cpp
source file and put following code into it.
#include "Platform/Core/Renderer.h"
#include "Platform/Core/Window.h"
#include "CoolWidgets/CoolWidgets.h"
#include "FormMain.h"
namespace nitisa
{
CFormMain *FormMain{ nullptr };
CFormMain::CFormMain() :
CForm(L"FormMain", CWindow::Create(), CRenderer::Create())
{
}
}
This one is also obvious. CForm constructor has additional first parameter in comparison to form prototypes and thus we added it (it just need the form class name, without "C" at the beginning). We also included main header file of our package we created earlier. We will use widget from it later.
And finally put following code into main.cpp
file.
#include "Platform/Core/Application.h" // Include application manager
#include "FormMain.h" // Include main form
#include <Windows.h> // Include windows declarations
// Import libraries
#pragma comment(lib, "Nitisa.lib") // Nitisa core. Required
#pragma comment(lib, "Standard.lib") // Standard widgets library
#pragma comment(lib, "Platform.lib") // Platform dependent classes implementations (CApplication, CRenderer, CWindow)
#pragma comment(lib, "opengl32.lib") // Renderer uses OpenGL graphics, so OpenGL library is required
#pragma comment(lib, "CoolWidgets.lib") // Link with CoolWidgets package library we are working on
using namespace nitisa;
int WINAPI WinMain(HINSTANCE, HINSTANCE, PSTR, INT)
{
CApplication app; // Create application
app.CreateForm(&FormMain); // Create main form
app.setMainForm(FormMain); // Tell application which form is the main one
app.Run(); // Run application
return 0;
}
It is almost exact copy of the corresponding files from other tutorials. We only added linking with our CoolWidgets package and added using nitisa namespace to simplify WinMain() function a little.
If you build and run application you will see an empty form. That is all we want from this application for now. Later, after creating a widget, we will add it on the form to check it works properly.
There are two major kinds of widgets in the Nitisa. The first one is called Component. Components are widgets that have no visual representation on a form. It's a widgets you can add to a form but when application is running you can't see it on a form. For example, it can be a timer component which run some task periodically or an image list component which stores a list of images which can be used by another widgets on a form. The other kind is called Control and controls are widgets which have visual representation on a form. It's such a common widgets like text inputs, labels, buttons and others. User can interact with controls as they are visible on a form. Both components and controls, as well as most everything in the framework, are described by their interfaces. Components are described by IComponent interface and controls are described by IControl interface. Instead of starting developing widget from interface it is almost already easier to start from a corresponding helper class which already has most common features implemented. For components and controls such classes are CComponent and CControl.
Our registration widget will be displayed on a form and user will be able to interact with it so it will be control and thus it should implement IControl interface and, as stated above, we will derive our widget class from CControl class.
Development of a widget is usually divided into two big parts. The first part is designing a widget. It includes adding all needed properties and methods for making widget do what it can as well as rendering methods. All this staff is implemented in a widget class. The second part is implementation of widget interactions. Interactions are driven by events occurred on a form or in a system. For example, when user clicks somewhere on a form, the form detects which widget should get notification of that click and passes it to that widget. All such notifications are handled in widget service. Widget service is implemented as a separate class and we will implement it in the next paragraph. In this paragraph we focus on implementing widget class.
It is accepted in the Nitisa to have interfaces for widgets as well. Those interfaces describe only minimum required features and should not have any layout customization members. All layout members should be in a widget's class, not in a widget's interface. Following that custom we will also declare an interface for our widget.
It is recommended to put components in Components directory in a package. Controls should be in the Controls directory, forms should be in the Forms directory and so on. In that directories should be interfaces for the components, controls, forms and so on respectfully. For each component, control, form there should be subdirectory called the same name the component, control, form is called. In the component, control, form subdirectory should be a header and source files with declaration and implementation of the corresponding component, control, form and so on. Primary files implementing component, control, form should have the same name the component, control, form has. Additionally everything in the package should be inside nitisa::[package-namespace]
namespace, where [package-namespace] is the namespace name as close as possible to the package name. So, for our widget we will create a folder called Controls in the CoolWidgets project. In that folder we will create a file called IRegistration.h with declaration of the interface for our widget (we are going to name the widget Registration). Inside Controls directory we will create subdirectory for the widget implementation called Registration and inside it we will have primary files Registration.h and Registration.cpp with declaration and implementation of the widget. We will have some additional files there but we will describe them later.
If you see on the Form Builder, you may find that most widgets have a lot of properties which allow to change widget appearance and behaviour. We won't add many to our widget. It will have only three properties for demonstration what property types widgets can have. The first property will be a simple one. We will call it BackgroundColor and it will control, as it's already clear from the name, the color of the background of the widget. The second one will control inputs border color and will have two states: normal state and the state when input is active or focused and user can type something in it. And the final property will be a complex one. It will store three parameters: first name, last name and date of birth. We will talk more about properties and their handling in the Form Builder in the last part of the tutorial.
The second property storing input border color depending on state, lets call it InputBorderColor, will have two states as we wrote before. The first state is a normal state, when input is not active. And the second one is when input is active (or focused). Usually there are much more states. For example, input can be under a mouse pointer or a pointer can be outside an input, also pointer can be down over the input and not released yet. Input can also be in disabled state if widget is disabled and so on. Our widget will have only two states for simplicity. To describe that states we are going to have following enumeration.
enum class State
{
Normal,
Active
};
For the third, the complicated one, property we define structure like following. It, as stated above, contains first and last name and date of birth in the three separate fields Year, Month and Day.
struct RegistrationData
{
String FirstName;
String LastName;
int Year;
int Month;
int Day;
};
We talked above of properties but classes in C++ don't have properties. By property we mean getter method for getting stored value and setter method for changing the value. So called readonly properties have only a getter method. In the Nitisa the name of a getter method is formed as get[PropertyName], where [PropertyName] is a name of a property. In our example it will be getBackgroundColor()
and getInputBorderColor()
methods. The name of setter method is formed similarly but with set prefix.
Besides properties widgets have events. Event is just a pointer to a function which will be called when something happens. We will add one event for our widget. That event will be called when user type first or last name or change date of birth. The event will be called OnChange.
When user clicks on the first name input of our widget we set that input as focused input. In the same way when user clicks on the second input (last name input), we set that input as focused. Our input for entering date of birth will be just a text shown currently selected date of birth and a small arrow in form of a triangle, by clicking on which a calendar will appear. So, we need to know somehow at which part of a widget user clicks. For that we are going to have helper method getElement(const PointF &p)
which will return a part of widget under a specified coordinate. The best way for specifying a widget part is an enumeration. We will have such parts as first name input, last name input, date of birth, date of birth arrow and calendar in it and define it as:
enum class Element
{
None,
InputFirstName,
InputLastName,
Date,
DateArrow,
Calendar
};
So the full declaration of the method is Element getElement(const PointF &p)
.
How does form know whether a mouse pointer is over a widget or not? It uses widget's IControl *getControl(const PointF &position)
method. The method return widget under a specified position or nullptr if neither widget which method is called nor its any child widget is under the specified position. The default implementation of the method from CControl class just uses widget rectangle returned by widget's getRect()
method and widget transformation including transformations of all parent widgets to determine whether the specified position is inside that rectangle or not. If widget has child widgets the method first called for all the child widgets. That is ideally suited for widgets with rectangular shape. What if widget's shape is not a rectangle? In this case the method should be overwritten and perform calculation taking into account the non-rectangular shape. Is our widget rectangular? Not quite. We can treat its shape as rectangle until calendar isn't shown. When calendar for selecting date of birth is shown our widget shape can be described by two rectangles: original rectangle with labels and inputs plus the rectangle of the calendar part. So we will overwrite the method so that a form could correctly process events when our widget have calendar opened.
By the way there are two more rectangles the control has. The first one is render rectangle. The render rectangle is just a widget rectangle with all the effects which take no part in interaction with widget. For example, widget can have shadow. When user clicks on the shadow nothing happens - shadow is just an effect and doesn't participate into interactions with widget. But the shadow should be taking into account when widget is being drawn. That is the purpose of the render rectangle of a widget. Render rectangle is being returned by the getRenderRect()
method of the widget. The last rectangle is a client rectangle. It is returned by the getClientRect()
method and is being used in alignment of child widgets on a widget. When widget has another widgets on it and that widgets have alignments different form Align::None widget uses client rectangle instead of all its rectangle to place child widgets. Default implementations of those three methods in CControl class return the same values equal to the rectangle of a control. Our widget doesn't have effects and child widgets so we don't need to overwrite any of these methods.
Control allows to set different cursor when mouse pointer is over it area by setCursor()
method. What if we want to have different cursors on different parts of a widget? Lets our widget has default cursor everywhere except when mouse pointer is over inputs and over the arrow for showing the calendar. When pointer is over text inputs we want it have "I-Beam" shape and when it is over the calendar showing arrow we want it to be in a form of hand pointer so that user understand the arrow can be clicked and some action will follow the click. How can we do that? Form uses getCursor()
method of a widget to determine which shape of widget should be shown. It does it all the time mouse pointer moves. So the answer is simple: we need to overwrite the method and return needed shape depending on what part of the widget is under the mouse pointer.
If your widget allows size changing you need to do nothing as that is the default behaviour and it is already implemented in the CControl class. Lets make our widget non-resizable to show how it can be done. There is a way to restrict widget size to certain range by changing its Constraints property. But changing the property still allows changes to that property from source code and it means if we just change constraints in the Form Builder user still will be able to change constraints to a new value in the source code. To completely prevent user from changing size of a widget we need to overwrite three methods. The first one is setSize()
which is obvious. The second one is the setConstraints()
which is less obvious (size cannot exceed constraints, so constraints effect size and thus constraints changes should be prevented as well). And the final way when the size of the widget can be changes is aligning. When, for instance, aligning is set to client area, the widget takes all available parent area independent on its constraints or current size. So, we need also prevent changing the alignment by overwriting the setAlign()
method. Although there are other constraints and size changing methods (like setMinWidth()
and setHeight()
) they all call setConstraints()
and setSize()
so we don't need to overwrite them.
And the last method of CControl we are going to override is the setEnabled()
method. Our widget will always be enabled.
Many widgets have common parts. For example, many widgets can have scroll bars when their content cannot be fit into the widget area. Another example is a field where user can type something. Text input, editable tables, even time picker can have it. Some of the most commonly used parts are already implemented in the framework and are called built-in controls. Sure you can implement all needed functionality by your own but in our example we want to show how to use some of the existing built-in controls. It will also greatly simplify the widget development.
So, lets see what built-in controls we need. We have two text inputs to type first and last name so we can use built-in TextInput control for them. We also have somehow to allow user to enter date of birth. In this case built-in MonthCalendar comes in handy.
Built-in controls cannot be used as is as they have abstract method(s). So, to use them we need to create class derived from built-in control we are going to use and implement those method(s). Fortunately for us all the standard built-in controls have only one abstract method called getControl()
and its implementation is trivial. It should just return control to which it belongs. So we will have two classes CTextInput
derived from CBuiltInTextInput and CMonthCalendar
derived from CBuiltInMonthCalendar. Built-in controls may notify parent control of some changes or actions the main control should take. For example, they may notify parent of needed repainting or of text/value change. To get that notifications parent widget should provide built-in control with listener. Listener is a small interface which methods will be called at certain events. To implement a listener we need to create a class derived from needed listener and implement its methods. All standard built-in controls have listeners. For our built-in controls they are IBuiltInTextInputListener and IBuiltInMonthCalendarListener. Their implementation is pretty simple as they both have only two methods. The first one is called when repaint is required and accordingly we just repaint everything and the second method of both of them is called when change of text or date is happened. When text is changed in input we just call our OnChange event. When date changed in calendar we also call the event and additionally close calendar and repaint widget.
We could have created built-in controls in the constructor of our widget but we will do it another way and this is why. The less objects you create at initialization the faster it finishes and the best way is not to do any job until it is required. Built-in calendar control is required only when user clicks on arrow to show it. User can never do it so why to create built-in calendar in constructor of the widget if it may never be used? So the built-in calendar control of our widget is a good candidate to be created on the fly - right when it is required. Creating only one small part is almost always fast enough to be done without any observable lag. So we will have helper method getCalendar()
which will create and setup built-in calendar if it is not created yet and, instead of accessing calendar via its variable, we will access it via this method. In our widget inputs are needed always if widget is created as they are visible all the time but we will anyway use the same method to create them and thus we will have two more helper methods getInputFirstName()
and getInputLastName()
.
Additionally we are going to need some more helper methods. We need two of them for open and close calendar, two for rendering our widget and rendering calendar. Also we are going to store current element under a mouse pointer (hovered element). Instead of finding it every time we need it we will store it in private variable and update its value only when mouse pointer position is changed. For that we will have UpdateHoveredElement()
method.
And finally we need a method to update our widget properties when its style is changed. As you might know widgets in Nitisa support styling. By default there is no style and all widgets are being displayed as they were designed. But user may create its own style and apply it to a form and/or to a widgets. The style may contain settings which are familiar for a widget and widget applies them in that case. Our widget has two properties BackgroundColor and InputBorderColor. Lets look for those properties in style and apply them if they are there. We also should not forget when creating custom widget that built-in controls can also support styling and thus we need to call theirs UpdateFromStyle()
methods so they can properly handle it.
Lets now put it all together. Create header file IRegistration.h in the Controls directory and put there following widget interface declaration code.
#pragma once
#include "Nitisa/Core/Strings.h" // We use String type declared here
#include "Nitisa/Interfaces/IControl.h" // IControl interface declaration is here
namespace nitisa // Everything should be in this global namespace
{
namespace coolwidgets // Namespace of our package
{
class IRegistration :public virtual IControl // Widget interface should be derived from control interface
{
public:
struct RegistrationData // Structure describing widget data
{
String FirstName;
String LastName;
// Date of birth
int Year;
int Month;
int Day;
};
public:
virtual RegistrationData getRegistrationData() = 0; // Getter method of RegistrationData property
virtual bool setRegistrationData(const RegistrationData &value) = 0; // Setter method of RegistrationData property
};
}
}
Widget interface is pretty simple. It has, accordingly to what was said earlier, only getter and setter methods for widget data and has no methods for layout properties. By the next step create Registration.h header file in the Controls\Registration directory and put following widget class declaration code in it.
#pragma once
#include "Nitisa/BuiltInControls/MonthCalendar/BuiltInMonthCalendar.h" // Built-in month calendar control is declared here
#include "Nitisa/BuiltInControls/TextInput/BuiltInTextInput.h" // Built-in text input control is declared here
#include "Nitisa/BuiltInControls/IBuiltInMonthCalendarListener.h" // Built-in month calendar control listener is declared here
#include "Nitisa/BuiltInControls/IBuiltInTextInputListener.h" // Built-in text input control listener is declared here
#include "Nitisa/Core/Align.h" // Align enumeration is declared here
#include "Nitisa/Core/Control.h" // CControl class is declared here
#include "Nitisa/Image/Color.h" // Color is declared here
#include "Nitisa/Math/PointF.h" // PointF is declared here
#include "Nitisa/Math/RectF.h" // RectF is declared here
#include "../IRegistration.h" // Include declaration of widget interface
namespace nitisa
{
// Forward declaration of interfaces we use
class IBuiltInTextInput;
class IBuiltInMonthCalendar;
class IControl;
class IForm;
class IRenderer;
class IStyle;
class ITexture;
namespace coolwidgets
{
class CRegistrationService; // Forward declaration of widget service
class CRegistration :public virtual IRegistration, public CControl // Widget class implement widget interface and is derived form default control implementation in CControl
{
friend CRegistrationService; // Widget service need to be a friend to have access to widget private properties and methods
public:
enum class State // Enumeration describing input states
{
Normal, // Normal state, not focused
Active // Input is focused
};
private:
enum class Element // Enumeration describing widget parts
{
None, // Nothing or irrelevant part
InputFirstName, // Input where user can type his first name
InputLastName, // Input where user can type his last name
Date, // Area where current date of birth is displayed
DateArrow, // Arrow which can be used to show calendar
Calendar // Calendar
};
class CTextInput :public CBuiltInTextInput // Built-in text input derived from its base class
{
private:
CRegistration *m_pControl;
public:
IControl *getControl() override; // Implement abstract method which should return parent widget
CTextInput(CRegistration *control);
};
class CTextInputListener :public virtual IBuiltInTextInputListener // Built-in text input listener implementation class derived from corresponding interface
{
private:
CRegistration *m_pControl;
public:
void NotifyOnRepaint(IBuiltInControl *sender, const RectF &rect) override; // Notification called when repainting is required
void NotifyOnTextChanged(IBuiltInControl *sender) override; // Notification called when text is changed in the input
CTextInputListener(CRegistration *control);
};
class CMonthCalendar :public CBuiltInMonthCalendar // Built-in month calendar derived from its base class
{
private:
CRegistration *m_pControl;
public:
IControl *getControl() override; // Implement abstract method which should return parent widget
CMonthCalendar(CRegistration *control);
};
class CMonthCalendarListener :public virtual IBuiltInMonthCalendarListener // Built-in month calendar listener implementation class derived from corresponding interface
{
private:
CRegistration *m_pControl;
public:
void NotifyOnRepaint(IBuiltInControl *sender, const RectF &rect) override; // Notification called when repainting is required
void NotifyOnChange(IBuiltInControl *sender) override; // Notification called when selected date is changed in month calendar
CMonthCalendarListener(CRegistration *control);
};
private:
Color m_sBackgroundColor; // We store BackgroundColor property value here
Color m_aInputBorderColor[(int)State::Active + 1]; // We store InputBorderColor property here. It has two values for each of states
IBuiltInTextInput *m_pInputFirstName; // We store built-in text input instance for first name here
IBuiltInTextInput *m_pInputLastName; // We store built-in text input instance for last name here
IBuiltInMonthCalendar *m_pCalendar; // We store built-in calendar instance here
Element m_eFocusedElement; // We store currently focused element here. It can be first name input, last name input or date of birth area
Element m_eHoveredElement; // We store element which is under mouse pointer here
Element m_eDownElement; // We store element at which mouse pointer was down but not yet released. Can be either first name input or last name input
ITexture *m_pCanvas; // Texture in which our widget will be drawn
ITexture *m_pCanvasCalendar; // Texture in which calendar part will be drawn
CTextInputListener m_cTextInputListener; // Listener for text inputs
CMonthCalendarListener m_cMonthCalendarListener; // Listener for calendar
bool m_bCalendarOpened; // It indicates whether calendar is shown or not
RectF m_sInputFirstNameRect; // Rectangle of the area with first name input
RectF m_sInputLastNameRect; // Rectangle of the area with last name input
RectF m_sDateRect; // Rectangle of the area with date of birth
RectF m_sCalendarBorderWidth; // Border widths of calendar
RectF m_sCalendarPadding; // Padding between borders and calendar
Color m_sCalendarBorderColor; // Color of calendar border
Color m_sCalendarBackgroundColor; // Color of calendar background
RectF m_sCalendarRect; // Calendar rectangle when it is shown
Element getElement(const PointF &p); // Find which element is under the specified point (in widget coordinate system)
IBuiltInTextInput *getInputFirstName(); // Return first name input. Create it if it was not created yet
IBuiltInTextInput *getInputLastName(); // Return last name input. Create it if it was not created yet
IBuiltInMonthCalendar *getCalendar(); // Return calendar. Create it if it was not created yet
void OpenCalendar(); // Show calendar
void CloseCalendar(); // Hide calendar
void UpdateFromStyle(IStyle *style); // Update properties from style
bool UpdateHoveredElement(const PointF &p); // Update m_eHoveredElement property when mouse pointer is in specified point (in widget coordinate system). Return true if hovered element was changed
void RenderControl(IRenderer *renderer); // Render widget
void RenderCalendar(IRenderer *renderer); // Render calendar
public:
void(*OnChange)(IControl *sender); // Event called when data is changed
// IControl getters
IControl *getControl(const PointF &position) override;
CursorType getCursor() override;
// IControl setters
bool setAlign(const Align value) override;
bool setConstraints(const RectF &value) override;
bool setSize(const PointF &value) override;
bool setEnabled(const bool value) override;
// IRegistration getters
RegistrationData getRegistrationData() override;
// IRegistration setters
bool setRegistrationData(const RegistrationData &value) override;
// Constructors & destructor
CRegistration();
CRegistration(IControl *parent);
CRegistration(IForm *parent);
~CRegistration() override;
// Layout property getters
Color getBackgroundColor();
Color getInputBorderColor(const State state);
// Layout property setters
bool setBackgroundColor(const Color &value);
bool setInputBorderColor(const State state, const Color &value);
};
}
}
As you may noticed we also added forward declaration for the widget service class and marked it as widget class friend. We will implement service in the next paragraph.
Built-in text input and calendar have no background. We need to define and draw it ourselves. Text inputs we will draw on the white background and for the calendar we defined m_sCalendarBorderWidth
, m_sCalendarPadding
, m_sCalendarBorderColor
and m_sCalendarBackgroundColor
members to define nice layout. You may set colors to transparent (last component is 0) later to see how month calendar looks by default. We don't add getters and setter for these properties. You may later do it so that they also be editable in the Form Builder.
We declared three constructors in the class. It is common practice to have exactly three of them. The first one just creates widget and has no arguments. The second one creates widget and places it on a specified form and the last one creates widget and places it on a specified another widget. The second or third constructor may be missing if widget cannot be placed on a form or on another widget. As we create built-in controls in the widget we need to destroy them somewhere when they are not needed anymore. For that we have the destructor.
Classes for built-in controls and corresponding listeners are declared inside a private section of the widget class. They are not supposed to be used by the end user so we make them unavailable in such a way. It is also allowed to make service private but it's more common to have it public as the user may want to slightly adjust a widget by overriding some of its methods and may be some methods of its service will need some adjustments as well. We will create service in the next paragraph and we will make it available for widget users.
Add these two header files to CoolWidgets.h file as shown below.
#pragma once
#include "Controls/IRegistration.h"
#include "Controls/Registration/Registration.h"
Although we declared built-in control classes and listeners inside the CRegistration
class, it's a good idea to put implementations into a separate source code files. So lets create file called TextInput.cpp in the Controls\Registration directory and put following built-in text input implementation there.
#include "stdafx.h"
namespace nitisa
{
namespace coolwidgets
{
CRegistration::CTextInput::CTextInput(CRegistration *control) :
CBuiltInTextInput(),
m_pControl{ control }
{
}
IControl *CRegistration::CTextInput::getControl()
{
return m_pControl;
}
}
}
The implementation is trivial. In constructor we just call parent class constructor and store pointer to widget class instance and in the getControl()
method we return stored widget class instance.
Create source code file MonthCalendar.cpp in the same directory and put following code in it.
#include "stdafx.h"
namespace nitisa
{
namespace coolwidgets
{
CRegistration::CMonthCalendar::CMonthCalendar(CRegistration *control) :
CBuiltInMonthCalendar(),
m_pControl{ control }
{
}
IControl * CRegistration::CMonthCalendar::getControl()
{
return m_pControl;
}
}
}
As you can see it does absolutely the same as previous class.
Create source code file called TextInputListener.cpp in the same directory and put following code into it.
#include "stdafx.h"
namespace nitisa
{
namespace coolwidgets
{
CRegistration::CTextInputListener::CTextInputListener(CRegistration *control) :
m_pControl{ control }
{
}
void CRegistration::CTextInputListener::NotifyOnRepaint(IBuiltInControl *sender, const RectF &rect)
{
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
m_pControl->Repaint(rect, true);
}
void CRegistration::CTextInputListener::NotifyOnTextChanged(IBuiltInControl *sender)
{
if (m_pControl->OnChange)
m_pControl->OnChange(m_pControl);
}
}
}
Built-in text input listener class implementation is also pretty simple. In constructor we just store pointer to the widget class instance. We need it as we use it later in other methods. In the NotifyOnRepaint()
notification method we repaint the widget. We will discuss canvas usage a little bit later when we will talk about widget drawing method. The only thing we do in the NotifyOnTextChanged()
notification is calling widget OnChange event if it has a callback function assigned.
Create source code file called MonthCalendarListener.cpp and put following code into it.
#include "stdafx.h"
namespace nitisa
{
namespace coolwidgets
{
CRegistration::CMonthCalendarListener::CMonthCalendarListener(CRegistration *control) :
m_pControl{ control }
{
}
void CRegistration::CMonthCalendarListener::NotifyOnRepaint(IBuiltInControl *sender, const RectF &rect)
{
if (m_pControl->m_pCanvasCalendar)
m_pControl->m_pCanvasCalendar->setValid(false);
m_pControl->Repaint(false);
}
void CRegistration::CMonthCalendarListener::NotifyOnChange(IBuiltInControl *sender)
{
m_pControl->CloseCalendar();
if (m_pControl->OnChange)
m_pControl->OnChange(m_pControl);
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
m_pControl->Repaint(false);
}
}
}
Built-in month calendar listener implementation is similar to the built-in text input listener implementation except for a change notification. Here we just hide (or close) the calendar, call event callback function if it is assigned and repaint widget.
Create Registration.cpp file in the Controls\Registration directory and put following code with widget class implementation in it.
#include "stdafx.h"
namespace nitisa
{
namespace coolwidgets
{
#pragma region Constructor & destructor
CRegistration::CRegistration():
// Call parent class constructor
CControl(L"Registration", true, true, false, true, false, true),
// Set default values for properties
m_sBackgroundColor{ 0, 0, 0, 0 },
m_aInputBorderColor{ Color{ 127, 127, 127, 255 }, Color{ 127, 127, 255, 255 } },
m_pInputFirstName{ nullptr },
m_pInputLastName{ nullptr },
m_pCalendar{ nullptr },
m_eFocusedElement{ Element::InputFirstName },
m_eHoveredElement{ Element::None },
m_eDownElement{ Element::None },
m_pCanvas{ nullptr },
m_pCanvasCalendar{ nullptr },
m_cTextInputListener{ this },
m_cMonthCalendarListener{ this },
m_bCalendarOpened{ false },
m_sInputFirstNameRect{ 152, 0, 300, 24 },
m_sInputLastNameRect{ 152, 28, 300, 52 },
m_sDateRect{ 152, 56, 300, 80 },
m_sCalendarBorderWidth{ 1, 1, 1, 1 },
m_sCalendarPadding{ 5, 3, 5, 3 },
m_sCalendarBorderColor{ 151, 151, 151, 255 },
m_sCalendarBackgroundColor{ 255, 255, 255, 255 },
OnChange{ nullptr }
{
setService(new CRegistrationService(this), true); // Create and set widget service additionally destroying default one
CControl::setSize(PointF{ 300, 80 }); // Set size of the widget
}
CRegistration::CRegistration(IControl *parent) :CRegistration()
{
setParent(parent);
}
CRegistration::CRegistration(IForm *parent) : CRegistration()
{
setForm(parent);
}
CRegistration::~CRegistration()
{
if (m_pInputFirstName)
m_pInputFirstName->Release();
if (m_pInputLastName)
m_pInputLastName->Release();
if (m_pCalendar)
m_pCalendar->Release();
}
#pragma endregion
#pragma region IControl getters
IControl *CRegistration::getControl(const PointF &position)
{
if (m_bCalendarOpened)
{
Vec4f v{ ntl::Inversed<float>(getTransformMatrix()) * Vec4f { position.X, position.Y, 0, 1 } }; // Convert "position" into widget coordinate space
if (v.X >= m_sCalendarRect.Left && v.X < m_sCalendarRect.Right && v.Y >= m_sCalendarRect.Top && v.Y < m_sCalendarRect.Bottom) // Check whether position is inside calendar rectangle
return this;
if (v.X >= 0 && v.X < getWidth() && v.Y >= 0 && v.Y < getHeight()) // Check whether position is inside widget rectangle
return this;
return nullptr; // Position is neither in calendar nor in widget rectangle, return nullptr
}
return CControl::getControl(position); // If calendar is not shown, default getControl() is OK for us
}
CursorType CRegistration::getCursor()
{
switch (m_eHoveredElement)
{
// If hovered element is either of text inputs, return I-Beam cursor type
case Element::InputFirstName:
case Element::InputLastName:
return CursorType::IBeam;
// If hovered element is arrow for showing calendar, return Hand cursor type
case Element::DateArrow:
return CursorType::Hand;
// In other cases return value assigned to Cursor property
default:
return CControl::getCursor();
}
}
#pragma endregion
#pragma region IControl setters
bool CRegistration::setAlign(const Align value)
{
// Aligning is not supported
return false;
}
bool CRegistration::setConstraints(const RectF &value)
{
// Constraints cannot be changed
return false;
}
bool CRegistration::setSize(const PointF &value)
{
// Size cannot be changed
return false;
}
bool CRegistration::setEnabled(const bool value)
{
// Widget is always enabled and this cannot be changed
return false;
}
#pragma endregion
#pragma region IRegistration getters
IRegistration::RegistrationData CRegistration::getRegistrationData()
{
// Return widget data getting values directly from built-in controls
return RegistrationData{
getInputFirstName()->getText(),
getInputLastName()->getText(),
getCalendar()->getYear(),
getCalendar()->getMonth(),
getCalendar()->getDay() };
}
#pragma endregion
#pragma region IRegistration setters
bool CRegistration::setRegistrationData(const RegistrationData &value)
{
// Set widget values directly in controls
CLockRepaint lock{ this };
bool result{ false };
result = getInputFirstName()->setText(value.FirstName) || result;
result = getInputLastName()->setText(value.LastName) || result;
result = getCalendar()->setYear(value.Year) || result;
result = getCalendar()->setMonth(value.Month) || result;
result = getCalendar()->setDay(value.Day) || result;
if (result)
{
// Repaint only if something was really changed
if (m_pCanvas)
m_pCanvas->setValid(false);
if (m_bCalendarOpened && m_pCanvasCalendar)
m_pCanvasCalendar->setValid(false);
Repaint(false);
}
return result;
}
#pragma endregion
#pragma region Getters
Color CRegistration::getBackgroundColor()
{
return m_sBackgroundColor;
}
Color CRegistration::getInputBorderColor(const State state)
{
return m_aInputBorderColor[(int)state];
}
#pragma endregion
#pragma region Setters
bool CRegistration::setBackgroundColor(const Color &value)
{
if (value != m_sBackgroundColor)
{
m_sBackgroundColor = value;
if (m_pCanvas)
m_pCanvas->setValid(false);
Repaint(false);
return true;
}
return false;
}
bool CRegistration::setInputBorderColor(const State state, const Color &value)
{
if (value != m_aInputBorderColor[(int)state])
{
m_aInputBorderColor[(int)state] = value;
if (m_pCanvas)
m_pCanvas->setValid(false);
Repaint(false);
return true;
}
return false;
}
#pragma endregion
#pragma region Helpers
CRegistration::Element CRegistration::getElement(const PointF &p)
{
// Check whether point is inside calendar rectangle
if (m_bCalendarOpened && p.X >= m_sCalendarRect.Left && p.X < m_sCalendarRect.Right && p.Y >= m_sCalendarRect.Top && p.Y < m_sCalendarRect.Bottom)
return Element::Calendar;
// Check whether point is inside first name built-in text input
if (p.X >= m_sInputFirstNameRect.Left + 2 && p.X < m_sInputFirstNameRect.Right - 2 && p.Y >= m_sInputFirstNameRect.Top + 2 && p.Y < m_sInputFirstNameRect.Bottom - 2)
return Element::InputFirstName;
// Check whether point is inside last name built-in text input
if (p.X >= m_sInputLastNameRect.Left + 2 && p.X < m_sInputLastNameRect.Right - 2 && p.Y >= m_sInputLastNameRect.Top + 2 && p.Y < m_sInputLastNameRect.Bottom - 2)
return Element::InputLastName;
// Check whether point is inside date of birth region
if (p.X >= m_sDateRect.Left && p.X < m_sDateRect.Right && p.Y >= m_sDateRect.Top && p.Y < m_sDateRect.Bottom)
{
// Check whether point is inside calendar show arrow region
if (p.X >= m_sDateRect.Right - m_sDateRect.height())
return Element::DateArrow;
return Element::Date;
}
return Element::None;
}
IBuiltInTextInput *CRegistration::getInputFirstName()
{
if (!m_pInputFirstName)
{
// If input is not yet created, create it and set its options
m_pInputFirstName = new CTextInput(this);
m_pInputFirstName->setSize(PointF{ m_sInputFirstNameRect.width() - 4, m_sInputFirstNameRect.height() - 4 });
m_pInputFirstName->setPosition(PointF{ m_sInputFirstNameRect.Left + 2, m_sInputFirstNameRect.Top + 2 });
m_pInputFirstName->setFocused(isFocused() && m_eFocusedElement == Element::InputFirstName);
m_pInputFirstName->setListener(&m_cTextInputListener);
}
return m_pInputFirstName;
}
IBuiltInTextInput *CRegistration::getInputLastName()
{
if (!m_pInputLastName)
{
// If input is not yet created, create it and set its options
m_pInputLastName = new CTextInput(this);
m_pInputLastName->setSize(PointF{ m_sInputLastNameRect.width() - 4, m_sInputLastNameRect.height() - 4 });
m_pInputLastName->setPosition(PointF{ m_sInputLastNameRect.Left + 2, m_sInputLastNameRect.Top + 2 });
m_pInputLastName->setFocused(isFocused() && m_eFocusedElement == Element::InputLastName);
m_pInputLastName->setListener(&m_cTextInputListener);
}
return m_pInputLastName;
}
IBuiltInMonthCalendar *CRegistration::getCalendar()
{
if (!m_pCalendar)
{
// If calendar is not yet created, create it and set its options
m_pCalendar = new CMonthCalendar(this);
m_pCalendar->setListener(&m_cMonthCalendarListener);
}
return m_pCalendar;
}
void CRegistration::OpenCalendar()
{
if (!m_bCalendarOpened)
{
m_bCalendarOpened = true; // Set flag indicating whether calendar is shown
m_eDownElement = Element::None; // Down element can be input only, so reset it to None just in case
PointF size{ getCalendar()->getRequiredSize() }; // Get calendar size
PointF disp{ m_sCalendarBorderWidth.Left + m_sCalendarPadding.Left, m_sCalendarBorderWidth.Top + m_sCalendarPadding.Top }; // Calculate where built-in calendar is placed relative to calendar area
float w{ disp.X + size.X + m_sCalendarBorderWidth.Right + m_sCalendarPadding.Right }; // Calculate total required width for calendar
float h{ disp.Y + size.Y + m_sCalendarBorderWidth.Bottom + m_sCalendarPadding.Bottom }; // Calculate total required height for calendar
// Update calendar rectangle with calculated values
m_sCalendarRect.Left = m_sDateRect.Right - w;
m_sCalendarRect.Right = m_sCalendarRect.Left + w;
m_sCalendarRect.Top = m_sDateRect.Bottom;
m_sCalendarRect.Bottom = m_sCalendarRect.Top + h;
getCalendar()->setSize(size); // Set built-in calendar size
getCalendar()->setPosition(disp); // Set built-in calendar position
getForm()->CaptureMouse(this, true); // Request to capture mouse input
if (m_pCanvasCalendar)
m_pCanvasCalendar->setValid(false); // Invalidate calendar canvas
Repaint(false); // Repaint widget
}
}
void CRegistration::CloseCalendar()
{
if (m_bCalendarOpened)
{
CLockRepaint lock{ this }; // Lock form's repaint to prevent multiple drawing until we finish with changes
Repaint(false); // Repaint widget area before hiding calendar
m_bCalendarOpened = false; // Set flag indicating whether calendar is shown
getCalendar()->NotifyOnFreeResources(); // Free resources used by built-in calendar
if (m_pCanvasCalendar)
{
// Free calendar canvas
m_pCanvasCalendar->Release();
m_pCanvasCalendar = nullptr;
}
if (isCaptureMouse())
getForm()->ReleaseCaptureMouse(); // Release mouse capture
}
}
bool CRegistration::UpdateHoveredElement(const PointF &p)
{
Element element{ getElement(p) }; // Find which element is under the specified point
if (element != m_eHoveredElement) // If current hovered element is different
{
switch (m_eHoveredElement) // Notify element which is loosing hovered state about it
{
case Element::InputFirstName:
getInputFirstName()->NotifyOnMouseLeave();
break;
case Element::InputLastName:
getInputLastName()->NotifyOnMouseLeave();
break;
case Element::Calendar:
getCalendar()->NotifyOnMouseLeave();
break;
}
m_eHoveredElement = element; // Update hovered element property
switch (m_eHoveredElement) // Notify new hovered element that he have got hovered state
{
case Element::InputFirstName:
getInputFirstName()->NotifyOnMouseHover(p - getInputFirstName()->getPosition());
break;
case Element::InputLastName:
getInputLastName()->NotifyOnMouseHover(p - getInputLastName()->getPosition());
break;
case Element::Calendar:
getCalendar()->NotifyOnMouseHover(p - m_sCalendarRect.LeftTop);
break;
}
return true;
}
return false;
}
void CRegistration::UpdateFromStyle(IStyle *style)
{
// Update widget properties
style->getOption(m_sClassName + L".BackgroundColor", m_sBackgroundColor);
style->getOption(m_sClassName + L".InputBorderColor[Normal]", m_aInputBorderColor[0]);
style->getOption(m_sClassName + L".InputBorderColor[Focused]", m_aInputBorderColor[1]);
// Update built-in controls properties
getInputFirstName()->UpdateFromStyle(style, m_sClassName + L".TextInput");
getInputLastName()->UpdateFromStyle(style, m_sClassName + L".TextInput");
getCalendar()->UpdateFromStyle(style, m_sClassName + L".MonthCalendar");
}
#pragma endregion
#pragma region Render
void CRegistration::RenderControl(IRenderer *renderer)
{
if (PrepareCanvas(renderer, &m_pCanvas, getSize())) // If we can draw on canvas
{
IFont *font{ getFont() }; // Store font instance
IPlatformFont *pf{ font->getPlatformFont(renderer) }; // Store platform font instance
String text_date{ nitisa::ToString(getCalendar()->getMonth()) + L"/" + nitisa::ToString(getCalendar()->getDay()) + L"/" + nitisa::ToString(getCalendar()->getYear()) }; // Make string with date in m/d/Y format
PointF text_first_name_size{ pf->getStringSize(L"First name:", font->Distance) }; // Calculate size of "First name:" string
PointF text_last_name_size{ pf->getStringSize(L"Last name:", font->Distance) }; // Calculate size of "Last name:" string
PointF text_date_of_birth_size{ pf->getStringSize(L"Date of birth:", font->Distance) }; // Calculate size of "Date of birth:" string
PointF text_date_size{ pf->getStringSize(text_date, font->Distance) }; // Calculate size of string with date
BlockColors normal{ m_aInputBorderColor[0], m_aInputBorderColor[0], m_aInputBorderColor[0], m_aInputBorderColor[0], Color{ 255, 255, 255, 255 }, Color{ 0, 0, 0, 0 } }; // Normal state block colors
BlockColors focused{ m_aInputBorderColor[1], m_aInputBorderColor[1], m_aInputBorderColor[1], m_aInputBorderColor[1], Color{ 255, 255, 255, 255 }, Color{ 0, 0, 0, 0 } }; // Focused state block colors
CStoreTarget s_target{ renderer }; // Store active target
CStorePrimitiveMatrix s_matrix{ renderer }; // Store active primitive matrix
renderer
->ActivateTarget(m_pCanvas) // Activate widget canvas to draw on it
->Clear(Color{ 0, 0, 0, 0 }) // Clear canvas
->DrawRectangle(getRect(), m_sBackgroundColor) // Draw widget background
->DrawBlock( // Draw first name input border and background
m_sInputFirstNameRect,
RectF{ 1, 1, 1, 1 },
RectF{ 0, 0, 0, 0 },
(isFocused() && m_eFocusedElement == Element::InputFirstName) ? focused : normal)
->DrawBlock( // Draw last name input border and background
m_sInputLastNameRect,
RectF{ 1, 1, 1, 1 },
RectF{ 0, 0, 0, 0 },
(isFocused() && m_eFocusedElement == Element::InputLastName) ? focused : normal)
->DrawBlock( // Draw date of birth area border and background
m_sDateRect,
RectF{ 1, 1, 1, 1 },
RectF{ 0, 0, 0, 0 },
(isFocused() && m_eFocusedElement == Element::Date) ? focused : normal)
// Draw first name input label
->ActivatePrimitiveMatrix(ntl::Mat4Translate<float>(148 - text_first_name_size.X, std::roundf(12 - text_first_name_size.Y * 0.5f), 0))
->DrawText(L"First name:", pf, font->Distance, CColors::Black)
// Draw last name input label
->ActivatePrimitiveMatrix(ntl::Mat4Translate<float>(148 - text_last_name_size.X, std::roundf(40 - text_last_name_size.Y * 0.5f), 0))
->DrawText(L"Last name:", pf, font->Distance, CColors::Black)
// Draw date of birth are label
->ActivatePrimitiveMatrix(ntl::Mat4Translate<float>(148 - text_date_of_birth_size.X, std::roundf(68 - text_date_of_birth_size.Y * 0.5f), 0))
->DrawText(L"Date of birth:", pf, font->Distance, CColors::Black)
// Draw date of birth date
->ActivatePrimitiveMatrix(ntl::Mat4Translate<float>(154, std::roundf(68 - text_date_size.Y * 0.5f), 0))
->DrawText(text_date, pf, font->Distance, CColors::Black)
// Draw calendar show/hide arrow
->ActivatePrimitiveMatrix(ntl::Mat4Translate<float>(m_sDateRect.Right - m_sDateRect.height(), m_sDateRect.Top, 0))
->DrawTriangle(PointF{ 6, 8 }, PointF{ m_sDateRect.height() - 6, 8 }, PointF{ m_sDateRect.height() * 0.5f, m_sDateRect.height() - 9 }, CColors::Black)
// Set primitive matrix to identity before drawing built-in inputs
->ActivatePrimitiveMatrix(nullptr);
// Draw built-in inputs
getInputFirstName()->Render(false, ntl::Mat4Translate<float>(m_sInputFirstNameRect.Left + 2, m_sInputFirstNameRect.Top + 2, 0), nullptr);
getInputLastName()->Render(false, ntl::Mat4Translate<float>(m_sInputLastNameRect.Left + 2, m_sInputLastNameRect.Top + 2, 0), nullptr);
// Make canvas valid
m_pCanvas->setValid(true);
}
DrawCanvas(renderer, getTransformMatrix(), m_pCanvas, PointF{ 0, 0 }); // Draw canvas on a form
}
void CRegistration::RenderCalendar(IRenderer *renderer)
{
if (!m_pCanvasCalendar || !m_pCanvasCalendar->isValid() || getCalendar()->isAnimating())
{
if (PrepareCanvas(renderer, &m_pCanvasCalendar, m_sCalendarRect))
{
CStoreTarget s_target{ renderer }; // Store active target
// Draw calendar background
renderer
->ActivateTarget(m_pCanvasCalendar)
->Clear(Color{ 0, 0, 0, 0 })
->DrawBlock(
RectF{ 0, 0, m_sCalendarRect.width(), m_sCalendarRect.height() },
RectF{ 1, 1, 1, 1 },
RectF{ 0, 0, 0, 0 },
BlockColors{ m_sCalendarBorderColor, m_sCalendarBorderColor, m_sCalendarBorderColor, m_sCalendarBorderColor, m_sCalendarBackgroundColor, Color{ 0, 0, 0, 0 } });
// Draw calendar built-in control
getCalendar()->Render(false, ntl::Mat4Translate<float>(getCalendar()->getLeft(), getCalendar()->getTop(), 0), nullptr);
m_pCanvasCalendar->setValid(true); // Make canvas valid
}
}
DrawCanvas(renderer, getTransformMatrix() * ntl::Mat4Translate<float>(m_sCalendarRect.Left, m_sCalendarRect.Top, 0), m_pCanvasCalendar, PointF{ 0, 0 }); // Draw calendar canvas on a form
}
#pragma endregion
}
}
We use #pragma region - #pragma endregion blocks. It does nothing to the source code, it just tells Visual Studio to make a block of all lines between region start and end which can be folded. It's just an another code organization tool. Widget class implementation can be large and it may be very useful to group some methods together like we did. It may greatly help to navigate through it. Please note that this type of making blocks of code is for Visual Studio only and it will raise warning if you compile such a code using another compilers. So you will need to add ignoring unknown pragma options to settings of all projects which are supposed to be compiled by another than Microsoft C++ compilers. To be particular, you need to add -Wno-unknown-pragmas options to Android and Linux projects to avoid warnings.
In the main constructor of widget class (the one without arguments) we call constructor of parent class and set default values for all needed properties of the class. CControl class constructor has a lot of arguments. Meaning of all of them you can find on the class reference page. Besides telling our widget class name, which is "Registration" (we need to omit C at the beginning) we told parent class that our widget can be placed both on a form and on another widget but no widgets can be placed on it, our widget can be focused and can be focused by Tab and Shift+Tab key combination, which is used to switch focus from one widget to another on a form, and our widget cannot be modal. In the first line of constructor body we replaced widget service with our own which we will implement in the next paragraph and set widget size to 300 by 80 pixels.
The rest two constructors just call the main one and put widget either on a form or on another widget. In destructor we just remove built-in controls instances if they where created.
In the getControl()
method, if the calendar is shown, we check whether the specified position is inside the calendar or widget rectangle and return our widget instance pointer if so or nullptr otherwise. The method argument is in the form coordinate system so we need first to convert it into widget coordinate system. Widget's getTransformMatrix()
method returns transformation matrix of the widget used to place it on a form. It takes into account that widget may have transformations (like rotation) as well as it may be placed not on a form directly but onto another widget. The method returns final transformation matrix. We will also use it in drawing method. So, to convert position from form coordinate system to widget one, all we need to do is apply inverted matrix to it which is done in the calculation of v variable.
getCursor()
method simply return cursor type depending on what element is under the mouse pointer at the moment.
Methods from IControl setters group just return false. It tells caller that nothing has been changed by the method call. We did it to prevent size changes as well as to make the widget always enabled.
getRegistrationData()
method takes values form built-in controls and return it in a form of RegistrationData
structure. Accordingly setRegistrationData()
does the opposite. It puts specified data into built-in controls and, if data differs, repaint the widget. You may have noticed that we didn't call event here although we change data. Setter methods are called from source code written by application developers and thus they new exactly when this happens and calling an event has no much sense. On contrary, event has sense when data is changed by a user who uses an application. That is why setter methods almost never call events.
Getters and setters of the two properties of the widget act the same.
In the getElement()
we find out which element is under the specified point. The point is specified in the widget coordinate space so we don't need any additional transformations. We defined input rectangles in m_sInputFirstNameRect
and m_sInputLastNameRect
rectangles but those rectangles are with border (1 pixel) and padding (1 pixel) so in the calculations in this method we add/subtract those 2 pixels. Also we assume that the calendar show arrow is a part of date of birth area and is located at the right of it. It has the height of the date of birth block height and the same width.
The next three methods for getting built-in text input and calendar instances just check if corresponding built-in control is already created and, if not, create it, set default parameters and corresponding listener. We do not set any parameters in the getCalendar()
method. We set them in the OpenCalendar()
method instead.
In the OpenCalendar()
method we calculate what size and position the calendar should have, update m_sCalendarRect
property, which stores both position and size in form of a rectangle, update built-in calendar properties, capture mouse input and repaint widget. Capture input means that all input events will be sent to the widget which captures the input instead of the widget which, for example, under the mouse pointer. In Nitisa keyboard and mouse input can be captured separately. We do not need keyboard input as the calendar handle mouse events only. Why do we need to capture mouse input? The answer is simple: we want to know where user clicks. If he clicks inside the calendar area, it means he works with calendar and we do nothing or close the calendar if he selects a date. But if user clicks outside the calendar area it means the calendar should be closed. If we didn't capture the input we would have missed clicks outside the calendar and widget area. Additionally, by the second argument of CaptureMouse()
methods, we told that the capture should be system wide. In this case we won't miss clicks even if they are outside a form. The CloseCalendar()
method hides the calendar.
UpdateHoveredElement()
finds out which element is under the specified point (which is also in the widget coordinate space) and, if it is another element that is stored in the m_eHoveredElement
member, update the member and depending of what element was hovered tells corresponding build-in control that element is not hovered anymore and, for the element which becomes hovered it tells that the element is hovered now. When telling element it is hovered the method should specify point in the corresponding built-in control coordinate space. We stored built-in text input positions in the built-in text inputs when created them, so the coordinate in the input coordinate space is just a coordinate in the widget space (which is p argument) minus the input position. The same is for the calendar but in this case calendar position is stored in the m_sCalendarRect
member.
UpdateFromStyle()
method will be called from widget service when style is changed. In this method we get values from style and update properties supported styling. As a style can have tons of properties for different widgets it is imperative to identify which properties are ours and which ones are not. To do this we suppose our widget properties in a style will have our widget class name at the beginning. This name is stored in m_sClassName
when we call CControl constructor (the first argument). We can choose any name for the properties in style. To allow style makers to use them you need include the names in the documentation of your widgets. The good choice can be something like namespace::classname.property or even company::namespace::classname.property. Anyway, the choice is your. Just don't forget it should be as much unique as possible to avoid using wrong style properties supposed for another widget with the same name.
The most important and most interesting methods in a widget class are usually the drawing methods. In our widget we have two. Most widgets don't draw themselves directly on a form. They usually draw themselves into an internal texture, usually called canvas, and then they draw this canvas on a form. This is a good optimization solution as the form repaint happens much more often then the change of a widget layout and drawing a simple image is way to faster than drawing all the elements of a widget each time. Our widget draw two parts separately. The first part is a widget without calendar and the second one is the calendar. For this parts we defined two canvas m_pCanvas
and m_pCanvasCalendar
and two drawing methods RenderControl()
and RenderCalendar()
accordingly.
At the beginning of drawing we need to ensure we need it. There is overloaded helper function PrepareCanvas() for that. All this function does is create canvas if it was not created earlier or set its size if it's not equal to required one. The function returns true if canvas was just created or its size was just changed or it's marked as invalid. In other cases it returns false and it means we can't or needn't to draw. When we draw our widget on a canvas we need to mark it as valid so that next time we omit drawing (unless the size of a widget and its canvas is changed). We do that by calling canvas method setValid(true)
. We do it near the end of the RenderControl()
method. The canvas remains in the valid state until we change it manually. So, in order to force drawing onto canvas again we need to call setValid(false)
. That is what we did in many methods earlier and left without explanation. See, for example, setBackgroundColor()
method of the widget class. In it, if new color differs from used one, we invalidate canvas and call Repaint()
function. This will trigger paint event later and the RenderControl()
method will be called from the widget service and PrepareCanvas()
will return true as the canvas is marked as invalid. By the way, it is very important to check whether canvas exists before working with it.
Lets now come back to the widget rendering. We use font and platform font to draw texts. As we use them multiple times, it is a good idea to store their instances in the variables to avoid multiple calls of the same methods. That is what we did in font and pf variables. Next, we declare string variable and put into it a string representation of a date stored in the calendar. The date is represented in the m/d/Y format. By the next four lines we get needed string sizes and store them in variables with type PointF.
By the next two lines we define color sets in form of BlockColors for background of inputs and date of birth. It's a set of six colors. The first four are left, top, right and bottom border colors. The fifth is the inside color and the last one is outside color. We use them in DrawBlock()
method of renderer.
Next two lines store renderer active target and primitive matrix. We need to store every active renderer state we change and restore them when we finish drawing. To do that we use two helper classes CStoreTarget and CStorePrimitiveMatrix.
After all the preparations we perform real drawing using renderer methods. To draw on a canvas (texture) we first activate it by ActivateTarget()
method, then we clear it by Clear()
method. After that we draw background of the widget. It is done with the call of DrawRectangle()
method. The next three calls of DrawBlock()
method draw backgrounds for inputs and date of birth part using either normal or focused color set depending whether part is focused or not.
After drawing background parts we draw all texts: three labels and date of birth. Renderer methods for drawing text don't have argument which allow to specify where the text should be drawn. Instead we need to use active primitive matrix. That is what we do: we activate primitive matrix which have text position and which is built using Mat4Translate() template function and then draw text. We use std::roundf()
for Y coordinate to avoid placing text in the middle of a pixel as we have multiplication by a 0.5f. That is not required for X coordinate.
Next we set identity primitive matrix as the built-in controls assume all the renderer settings they don't use are set to default values. After it we call built-in text inputs Render()
method to draw them. And finally we mark canvas as valid one.
All that drawing we perform if we need and we can accordingly to PrepareCanvas() function. At the end we draw canvas on a form with helper function DrawCanvas(). That is how we draw our widget.
The last method RenderCalendar()
perform drawing of the calendar in the similar but more simple way as there are much less parts to draw. Also it draws into m_pCanvasCalendar
canvas and draw that canvas on the form in the place where the calendar is. It is done by multiplying widget transformation matrix by matrix with calendar position in widget.
There are many events happen while application is running. User may change language, click somewhere in the application form, move mouse pointer, type something using keyboard or even shutdown the PC. All those events as well as a lot more others can be handled by application. In the Nitisa they are divided into notifications and events. Notifications is what the form and widget uses internally and theirs behaviour is defined during form or widget development. Events are used by the end users, users who use already existing widgets and forms. Event is customizable behaviour of a widget (or a form) while notification is not. In the most cases notifications and events go together. For example, there are both LeftMouseButtonDown notification and event. Notifications are methods defined and implemented at the widget development. Events are also defined at the widget development by the do nothing by default. Events are just properties to which a user defined callback function can be assigned and it will be called if corresponding event happens. The same widget can have different logic in different applications assigned to the same event. Notifications are fixed. They behave the same way for all instances of a widget.
So, notifications define widget response on certain events, like mouse move. All notifications are implemented in widget service. Service is an interface with a set of methods. That interface is usually separated from widget. That is done to avoid mistakes like calling notification methods by end user as all the notifications are responses for some events. Although some events are generated by user actions, user do it by moving mouse, type text, but not by calling notification method of a widget. So they are implemented separately from a widget class. There is an interface called IControlService which describes all notifications a control can have in the framework. There is similar interface called IComponentService for component service and so on. Besides notifications services can have (and they actually do) some other helper methods. For example, control interface has such methods as setParent()
, MoveControl()
and others. They are needed for proper interaction between widgets and a form. In 99.9% of cases you need only notification methods from services. Service interfaces have corresponding default implementations in classes called the same way just starting from C instead of I. For example, control service interface default implementation is called CControlService. CControl class creates instance of that default service implementation and assign it to itself to work with. But all default service implementations have empty notification methods. It means all notifications are ignored, widget doesn't react to user actions. That means we need to override notification methods and put there some logic to make widget respond to events.
When we have service class implemented we need to assign it to our widget. It can be done using setService()
method of a widget. That is what we did in the widget constructor.
In the control service interface there are a lot of notifications. We will use default control service class implementation in the CControlService class so we need only implement those notification our control is interested in. For example, our control doesn't have any child controls so it isn't interested in child control notifications at all. So, what are the notifications our widget need to handle?
Every widget that uses platform dependent resources need to implement NotifyOnFreeResources()
notification method. The main platform dependent resources are timers, textures and platform fonts. This notification is called when something happen that require to release all the platform resources acquired. We use textures draw on and we have built-in controls which might also use platform resources, so we need implement this notification.
Built-in controls also have NotifyOnAttach()
notification. So we, at least, need to pass these notification to them. The notifications the built-in controls have are described by their interfaces. All built-in controls are derived from IBuiltInControl interface and so you may find notification they all have in the reference page of that interface. Some of them may not be relevant. For example, they have NotifyOnDeactivate()
notification. It should be called when widget loses active state but our widget never gets active state, so it will never receive notification about loosing such a state and thus we have nothing to transfer to built-in controls. Thus this notification can be omitted.
Built-in month calendar draw some texts and support internationalization. This means that we need to handle NotifyOnTranslateChange()
notification as well. In our example we will only close the calendar. We won't change texts of the widget. But when creating production widgets you may want to handle application layout language change as well. All you need in this case is to load proper texts using ITranslate interface which you can get as Application->Translate
member and after it repaint the widget.
As our widget and built-in controls we use support styling we need to handle style changing notifications. There are two of them. The NotifyOnParenStyleChange()
notifications is called when one of the parent (widget or form) style is changed and our widget uses this parent style. The second notification is NotifyOnStyleChange()
is called when the style of the widget is changed. It happens when widget has his own style assigned. In the same way we need to handle font changes as we draw texts using font. The font change notifications are NotifyOnParentFontChange()
and NotifyOnFontChange()
.
We also need to know when our widget get and loose input focus. The notifications are NotifyOnSetFocus()
and NotifyOnKillFocus()
. We don't need notification about getting mouse capture because that happens only by our request. We do it on showing the calendar and will do when user downs left mouse button over any text input. But we do need the opposite notification NotifyOnKillCaptureMouse()
as capture loose may happen independently on our desire and we need handle it properly.
Sure we need a paint notification to know when widget should be repainted. There are two paint notifications. The first one is NotifyOnPaint()
. This notification is called at the beginning of drawing. The second one is NotifyOnPaintEnd()
. This notification is called at the end of drawing (after drawing of all child controls of a widget). We will draw our widget in the first one and won't use the second notification.
As we have inputs where user may type texts we definitely need to handle keyboard notifications. Moreover, we need to handle all of them as all of them are defined and might be used in the built-in controls. Additionally we will switch focused/active part of our widget by Up and Down arrows so user can use this keys to select what he wants to type: first name, last name or change date of birth. Also when date of birth part is active we will use F4 key to show and hide calendar.
Most of the interaction with widgets are done via mouse and our widget is not an exception. This means we need to handle mouse events. We need all of them except for scrolling ones as our widget has no scrolling parts.
And the final notification we will handle is the NotifyOnPasteString()
one. This notification is called when user pastes string from clipboard.
So, create a header file with name RegistrationService.h in the Controls\Registration directory and put following code inside it. Don't forget to add it to the CoolWidgets.h
#pragma once
#include "Nitisa/Core/ControlService.h"
namespace nitisa
{
namespace coolwidgets
{
class CRegistration;
class CRegistrationService :public CControlService
{
private:
CRegistration *m_pControl;
void CancelDown(const bool release_capture);
public:
// State change notifications
void NotifyOnAttach() override;
void NotifyOnFreeResources() override;
// Application notifications
void NotifyOnTranslateChange() override;
// Notifications from parent control
void NotifyOnParentStyleChange() override;
void NotifyOnParentFontChange() override;
// State change notifications
void NotifyOnStyleChange() override;
void NotifyOnFontChange() override;
void NotifyOnSetFocus(const MessageFocus &m) override;
void NotifyOnKillFocus() override;
void NotifyOnKillCaptureMouse() override;
// Paint notifications
void NotifyOnPaint(const MessagePaint &m, bool &draw_children) override;
// Keyboard input notifications
void NotifyOnKeyDown(const MessageKey &m, bool &processed) override;
void NotifyOnKeyUp(const MessageKey &m, bool &processed) override;
void NotifyOnChar(const MessageChar &m, bool &processed) override;
void NotifyOnDeadChar(const MessageChar &m, bool &processed) override;
// Mouse input notifications
void NotifyOnMouseHover(const MessagePosition &m) override;
void NotifyOnMouseLeave() override;
void NotifyOnMouseMove(const MessageMouse &m, bool &processed) override;
void NotifyOnLeftMouseButtonDown(const MessageMouse &m, bool &processed) override;
void NotifyOnLeftMouseButtonUp(const MessageMouse &m, bool &processed) override;
void NotifyOnLeftMouseButtonDoubleClick(const MessageMouse &m, bool &processed) override;
void NotifyOnRightMouseButtonDown(const MessageMouse &m, bool &processed) override;
void NotifyOnRightMouseButtonUp(const MessageMouse &m, bool &processed) override;
void NotifyOnRightMouseButtonDoubleClick(const MessageMouse &m, bool &processed) override;
void NotifyOnMiddleMouseButtonDown(const MessageMouse &m, bool &processed) override;
void NotifyOnMiddleMouseButtonUp(const MessageMouse &m, bool &processed) override;
void NotifyOnMiddleMouseButtonDoubleClick(const MessageMouse &m, bool &processed) override;
// Clipboard notifications
void NotifyOnPasteString(const MessagePasteString &m) override;
CRegistrationService(CRegistration *control);
};
}
}
Here we additionally declared helper method CancelDown()
which we will use every time we need to revert changes made when left mouse button was down.
Create source code file called RegistrationService.cpp in Controls\Registration directory and put following code into it.
#include "stdafx.h"
namespace nitisa
{
namespace coolwidgets
{
#pragma region Constructor & destructor
CRegistrationService::CRegistrationService(CRegistration *control):
CControlService(control),
m_pControl{ control }
{
}
#pragma endregion
#pragma region Helpers
void CRegistrationService::CancelDown(const bool release_capture)
{
if (m_pControl->m_eDownElement != CRegistration::Element::None)
{
// Send cancel down notification to the input on which down was done
switch (m_pControl->m_eDownElement)
{
case CRegistration::Element::InputFirstName:
m_pControl->getInputFirstName()->NotifyOnMouseDownCancel();
break;
case CRegistration::Element::InputLastName:
m_pControl->getInputLastName()->NotifyOnMouseDownCancel();
break;
}
m_pControl->m_eDownElement = CRegistration::Element::None;
}
if (release_capture && m_pControl->isCaptureMouse())
m_pControl->getForm()->ReleaseCaptureMouse();
}
#pragma endregion
#pragma region State change notifications
void CRegistrationService::NotifyOnAttach()
{
if (m_pControl->getForm())
{
m_pControl->getInputFirstName()->NotifyOnAttach();
m_pControl->getInputLastName()->NotifyOnAttach();
m_pControl->getCalendar()->NotifyOnAttach();
}
}
void CRegistrationService::NotifyOnFreeResources()
{
// Close calendar if it's opened
m_pControl->CloseCalendar();
// Delete both canvas if they are created
if (m_pControl->m_pCanvas)
{
m_pControl->m_pCanvas->Release();
m_pControl->m_pCanvas = nullptr;
}
if (m_pControl->m_pCanvasCalendar)
{
m_pControl->m_pCanvasCalendar->Release();
m_pControl->m_pCanvasCalendar = nullptr;
}
// Notify built-in controls
if (m_pControl->m_pInputFirstName)
m_pControl->m_pInputFirstName->NotifyOnFreeResources();
if (m_pControl->m_pInputLastName)
m_pControl->m_pInputLastName->NotifyOnFreeResources();
if (m_pControl->m_pCalendar)
m_pControl->m_pCalendar->NotifyOnFreeResources();
}
#pragma endregion
#pragma region Application notifications
void CRegistrationService::NotifyOnTranslateChange()
{
m_pControl->CloseCalendar();
}
#pragma endregion
#pragma region Notifications from parent control
void CRegistrationService::NotifyOnParentStyleChange()
{
if (m_pControl->getStyle())
{
m_pControl->CloseCalendar();
m_pControl->UpdateFromStyle(m_pControl->getStyle());
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
}
}
void CRegistrationService::NotifyOnParentFontChange()
{
m_pControl->CloseCalendar();
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
}
#pragma endregion
#pragma region State change notifications
void CRegistrationService::NotifyOnStyleChange()
{
if (m_pControl->getStyle())
{
m_pControl->CloseCalendar();
m_pControl->UpdateFromStyle(m_pControl->getStyle());
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
}
}
void CRegistrationService::NotifyOnFontChange()
{
m_pControl->CloseCalendar();
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
}
void CRegistrationService::NotifyOnSetFocus(const MessageFocus &m)
{
if (m.FocusedBy != FocusedBy::LeftMouse)
{
m_pControl->m_eFocusedElement = CRegistration::Element::InputFirstName;
m_pControl->getInputFirstName()->setFocused(true);
m_pControl->getInputLastName()->setFocused(false);
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
m_pControl->Repaint(false);
}
}
void CRegistrationService::NotifyOnKillFocus()
{
m_pControl->CloseCalendar();
m_pControl->m_eFocusedElement = CRegistration::Element::None;
m_pControl->getInputFirstName()->setFocused(false);
m_pControl->getInputLastName()->setFocused(false);
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
m_pControl->Repaint(false);
}
void CRegistrationService::NotifyOnKillCaptureMouse()
{
CancelDown(false);
}
#pragma endregion
#pragma region Paint notifications
void CRegistrationService::NotifyOnPaint(const MessagePaint &m, bool &draw_children)
{
if (!m.LastPass)
m_pControl->RenderControl(m_pControl->getForm()->getRenderer());
else if (m_pControl->m_bCalendarOpened)
m_pControl->RenderCalendar(m_pControl->getForm()->getRenderer());
}
#pragma endregion
#pragma region Keyboard input notifications
void CRegistrationService::NotifyOnKeyDown(const MessageKey &m, bool &processed)
{
bool changed{ false }, numlock{ Application->Keyboard->isToggled(Key::NumLock) }, ctrl, alt, shift;
Application->Keyboard->getControlKeys(ctrl, alt, shift);
switch (m_pControl->m_eFocusedElement)
{
case CRegistration::Element::InputFirstName:
if (m_pControl->getInputFirstName()->NotifyOnKeyDown(m.Key, ctrl, alt, shift, numlock))
changed = true;
break;
case CRegistration::Element::InputLastName:
if (m_pControl->getInputLastName()->NotifyOnKeyDown(m.Key, ctrl, alt, shift, numlock))
changed = true;
break;
case CRegistration::Element::Date:
if (m_pControl->m_bCalendarOpened && m_pControl->getCalendar()->NotifyOnKeyDown(m.Key, ctrl, alt, shift, numlock))
changed = true;
break;
}
if (changed)
{
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
if (m_pControl->m_pCanvasCalendar)
m_pControl->m_pCanvasCalendar->setValid(false);
m_pControl->Repaint(false);
}
}
void CRegistrationService::NotifyOnKeyUp(const MessageKey &m, bool &processed)
{
bool changed{ false }, numlock{ Application->Keyboard->isToggled(Key::NumLock) }, ctrl, alt, shift;
Application->Keyboard->getControlKeys(ctrl, alt, shift);
if (!ctrl && !alt && !shift)
{
switch (m.Key)
{
case Key::Up:
switch (m_pControl->m_eFocusedElement)
{
case CRegistration::Element::InputLastName:
m_pControl->m_eFocusedElement = CRegistration::Element::InputFirstName;
m_pControl->getInputLastName()->setFocused(false);
m_pControl->getInputFirstName()->setFocused(true);
break;
case CRegistration::Element::Date:
m_pControl->m_eFocusedElement = CRegistration::Element::InputLastName;
m_pControl->getInputLastName()->setFocused(true);
break;
default:
m_pControl->m_eFocusedElement = CRegistration::Element::Date;
m_pControl->getInputFirstName()->setFocused(false);
break;
}
if (m_pControl->m_bCalendarOpened)
m_pControl->CloseCalendar();
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
if (m_pControl->m_pCanvasCalendar)
m_pControl->m_pCanvasCalendar->setValid(false);
m_pControl->Repaint(false);
return;
case Key::Down:
switch (m_pControl->m_eFocusedElement)
{
case CRegistration::Element::InputFirstName:
m_pControl->m_eFocusedElement = CRegistration::Element::InputLastName;
m_pControl->getInputFirstName()->setFocused(false);
m_pControl->getInputLastName()->setFocused(false);
break;
case CRegistration::Element::InputLastName:
m_pControl->m_eFocusedElement = CRegistration::Element::Date;
m_pControl->getInputLastName()->setFocused(false);
break;
default:
m_pControl->m_eFocusedElement = CRegistration::Element::InputFirstName;
m_pControl->getInputFirstName()->setFocused(true);
break;
}
if (m_pControl->m_bCalendarOpened)
m_pControl->CloseCalendar();
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
if (m_pControl->m_pCanvasCalendar)
m_pControl->m_pCanvasCalendar->setValid(false);
m_pControl->Repaint(false);
return;
case Key::F4:
if (m_pControl->m_eFocusedElement == CRegistration::Element::Date)
{
m_pControl->m_bCalendarOpened ? m_pControl->CloseCalendar() : m_pControl->OpenCalendar();
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
if (m_pControl->m_pCanvasCalendar)
m_pControl->m_pCanvasCalendar->setValid(false);
m_pControl->Repaint(false);
return;
}
break;
}
}
switch (m_pControl->m_eFocusedElement)
{
case CRegistration::Element::InputFirstName:
if (m_pControl->getInputFirstName()->NotifyOnKeyUp(m.Key, ctrl, alt, shift, numlock))
changed = true;
break;
case CRegistration::Element::InputLastName:
if (m_pControl->getInputLastName()->NotifyOnKeyUp(m.Key, ctrl, alt, shift, numlock))
changed = true;
break;
case CRegistration::Element::Date:
if (m_pControl->m_bCalendarOpened && m_pControl->getCalendar()->NotifyOnKeyUp(m.Key, ctrl, alt, shift, numlock))
changed = true;
break;
}
if (changed)
{
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
if (m_pControl->m_pCanvasCalendar)
m_pControl->m_pCanvasCalendar->setValid(false);
m_pControl->Repaint(false);
}
}
void CRegistrationService::NotifyOnChar(const MessageChar &m, bool &processed)
{
bool changed{ false }, numlock{ Application->Keyboard->isToggled(Key::NumLock) }, ctrl, alt, shift;
Application->Keyboard->getControlKeys(ctrl, alt, shift);
switch (m_pControl->m_eFocusedElement)
{
case CRegistration::Element::InputFirstName:
if (m_pControl->getInputFirstName()->NotifyOnChar(m.Char, ctrl, alt, shift, numlock))
changed = true;
break;
case CRegistration::Element::InputLastName:
if (m_pControl->getInputLastName()->NotifyOnChar(m.Char, ctrl, alt, shift, numlock))
changed = true;
break;
case CRegistration::Element::Date:
if (m_pControl->m_bCalendarOpened && m_pControl->getCalendar()->NotifyOnChar(m.Char, ctrl, alt, shift, numlock))
changed = true;
break;
}
if (changed)
{
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
if (m_pControl->m_pCanvasCalendar)
m_pControl->m_pCanvasCalendar->setValid(false);
m_pControl->Repaint(false);
}
}
void CRegistrationService::NotifyOnDeadChar(const MessageChar &m, bool &processed)
{
bool changed{ false }, numlock{ Application->Keyboard->isToggled(Key::NumLock) }, ctrl, alt, shift;
Application->Keyboard->getControlKeys(ctrl, alt, shift);
switch (m_pControl->m_eFocusedElement)
{
case CRegistration::Element::InputFirstName:
if (m_pControl->getInputFirstName()->NotifyOnDeadChar(m.Char, ctrl, alt, shift, numlock))
changed = true;
break;
case CRegistration::Element::InputLastName:
if (m_pControl->getInputLastName()->NotifyOnDeadChar(m.Char, ctrl, alt, shift, numlock))
changed = true;
break;
case CRegistration::Element::Date:
if (m_pControl->m_bCalendarOpened && m_pControl->getCalendar()->NotifyOnDeadChar(m.Char, ctrl, alt, shift, numlock))
changed = true;
break;
}
if (changed)
{
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
if (m_pControl->m_pCanvasCalendar)
m_pControl->m_pCanvasCalendar->setValid(false);
m_pControl->Repaint(false);
}
}
#pragma endregion
#pragma region Mouse input notifications
void CRegistrationService::NotifyOnMouseHover(const MessagePosition &m)
{
if (m_pControl->UpdateHoveredElement(m_pControl->FormToLocal(m.Position)))
{
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
if (m_pControl->m_pCanvasCalendar)
m_pControl->m_pCanvasCalendar->setValid(false);
m_pControl->Repaint(false);
}
}
void CRegistrationService::NotifyOnMouseLeave()
{
if (m_pControl->UpdateHoveredElement(m_pControl->FormToLocal((PointF)m_pControl->getForm()->ScreenToClient(Application->Mouse->getPosition()))))
{
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
if (m_pControl->m_pCanvasCalendar)
m_pControl->m_pCanvasCalendar->setValid(false);
m_pControl->Repaint(false);
}
}
void CRegistrationService::NotifyOnMouseMove(const MessageMouse &m, bool &processed)
{
bool changed{ m_pControl->UpdateHoveredElement(m_pControl->FormToLocal(m.Position)) };
if (m_pControl->m_eDownElement != CRegistration::Element::None)
{
switch (m_pControl->m_eDownElement)
{
case CRegistration::Element::InputFirstName:
if (m_pControl->getInputFirstName()->NotifyOnMouseMove(m_pControl->FormToLocal(m.Position) - m_pControl->getInputFirstName()->getPosition(), m.Left, m.Middle, m.Right, m.Ctrl, m.Alt, m.Shift))
{
changed = true;
processed = true;
}
break;
case CRegistration::Element::InputLastName:
if (m_pControl->getInputLastName()->NotifyOnMouseMove(m_pControl->FormToLocal(m.Position) - m_pControl->getInputLastName()->getPosition(), m.Left, m.Middle, m.Right, m.Ctrl, m.Alt, m.Shift))
{
changed = true;
processed = true;
}
break;
}
if (changed)
{
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
if (m_pControl->m_pCanvasCalendar)
m_pControl->m_pCanvasCalendar->setValid(false);
m_pControl->Repaint(false);
}
return;
}
switch (m_pControl->m_eHoveredElement)
{
case CRegistration::Element::InputFirstName:
if (m_pControl->getInputFirstName()->NotifyOnMouseMove(m_pControl->FormToLocal(m.Position) - m_pControl->getInputFirstName()->getPosition(), m.Left, m.Middle, m.Right, m.Ctrl, m.Alt, m.Shift))
changed = true;
break;
case CRegistration::Element::InputLastName:
if (m_pControl->getInputLastName()->NotifyOnMouseMove(m_pControl->FormToLocal(m.Position) - m_pControl->getInputLastName()->getPosition(), m.Left, m.Middle, m.Right, m.Ctrl, m.Alt, m.Shift))
changed = true;
break;
case CRegistration::Element::Calendar:
if (m_pControl->getCalendar()->NotifyOnMouseMove(m_pControl->FormToLocal(m.Position) - m_pControl->m_sCalendarRect.LeftTop, m.Left, m.Middle, m.Right, m.Ctrl, m.Alt, m.Shift))
changed = true;
break;
}
if (changed)
{
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
if (m_pControl->m_pCanvasCalendar)
m_pControl->m_pCanvasCalendar->setValid(false);
m_pControl->Repaint(false);
}
}
void CRegistrationService::NotifyOnLeftMouseButtonDown(const MessageMouse &m, bool &processed)
{
if (m_pControl->m_eDownElement == CRegistration::Element::None && !m.Middle && !m.Right && !m.Ctrl && !m.Alt && !m.Shift)
{
bool changed{ false };
switch (m_pControl->m_eHoveredElement)
{
case CRegistration::Element::InputFirstName:
if (m_pControl->getInputFirstName()->NotifyOnMouseLeftDown(m_pControl->FormToLocal(m.Position) - m_pControl->getInputFirstName()->getPosition(), false, false, false, false, false))
changed = true;
m_pControl->m_eDownElement = CRegistration::Element::InputFirstName;
m_pControl->m_eFocusedElement = CRegistration::Element::InputFirstName;
m_pControl->getInputLastName()->setFocused(false);
m_pControl->getInputFirstName()->setFocused(true);
if (m_pControl->m_bCalendarOpened)
m_pControl->CloseCalendar();
break;
case CRegistration::Element::InputLastName:
if (m_pControl->getInputLastName()->NotifyOnMouseLeftDown(m_pControl->FormToLocal(m.Position) - m_pControl->getInputLastName()->getPosition(), false, false, false, false, false))
changed = true;
m_pControl->m_eDownElement = CRegistration::Element::InputLastName;
m_pControl->m_eFocusedElement = CRegistration::Element::InputLastName;
m_pControl->getInputFirstName()->setFocused(false);
m_pControl->getInputLastName()->setFocused(true);
if (m_pControl->m_bCalendarOpened)
m_pControl->CloseCalendar();
break;
case CRegistration::Element::Calendar:
m_pControl->getInputLastName()->setFocused(false);
m_pControl->getInputFirstName()->setFocused(false);
m_pControl->m_eFocusedElement = CRegistration::Element::Date;
if (m_pControl->getCalendar()->NotifyOnMouseLeftDown(m_pControl->FormToLocal(m.Position) - m_pControl->m_sCalendarRect.LeftTop, m.Middle, m.Right, m.Ctrl, m.Alt, m.Shift))
changed = true;
break;
case CRegistration::Element::Date:
m_pControl->getInputLastName()->setFocused(false);
m_pControl->getInputFirstName()->setFocused(false);
m_pControl->m_eFocusedElement = CRegistration::Element::Date;
if (m_pControl->m_bCalendarOpened)
m_pControl->CloseCalendar();
break;
case CRegistration::Element::DateArrow:
m_pControl->getInputLastName()->setFocused(false);
m_pControl->getInputFirstName()->setFocused(false);
m_pControl->m_eFocusedElement = CRegistration::Element::Date;
m_pControl->m_bCalendarOpened ? m_pControl->CloseCalendar() : m_pControl->OpenCalendar();
changed = true;
break;
default:
if (m_pControl->m_bCalendarOpened)
{
changed = true;
m_pControl->CloseCalendar();
}
}
if (changed)
{
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
if (m_pControl->m_pCanvasCalendar)
m_pControl->m_pCanvasCalendar->setValid(false);
m_pControl->Repaint(false);
if (m_pControl->m_eDownElement != CRegistration::Element::None)
m_pControl->getForm()->CaptureMouse(m_pControl, true);
}
}
else
CancelDown(true);
}
void CRegistrationService::NotifyOnLeftMouseButtonUp(const MessageMouse &m, bool &processed)
{
bool changed{ false };
if (m_pControl->m_eDownElement != CRegistration::Element::None)
{
switch (m_pControl->m_eDownElement)
{
case CRegistration::Element::InputFirstName:
if (m_pControl->getInputFirstName()->NotifyOnMouseLeftUp(m_pControl->FormToLocal(m.Position) - m_pControl->getInputFirstName()->getPosition(), m.Middle, m.Right, m.Ctrl, m.Alt, m.Shift))
changed = true;
break;
case CRegistration::Element::InputLastName:
if (m_pControl->getInputLastName()->NotifyOnMouseLeftUp(m_pControl->FormToLocal(m.Position) - m_pControl->getInputLastName()->getPosition(), m.Middle, m.Right, m.Ctrl, m.Alt, m.Shift))
changed = true;
break;
}
CancelDown(true);
if (changed)
{
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
m_pControl->Repaint(false);
}
processed = true;
return;
}
switch (m_pControl->m_eHoveredElement)
{
case CRegistration::Element::InputFirstName:
if (m_pControl->getInputFirstName()->NotifyOnMouseLeftUp(m_pControl->FormToLocal(m.Position) - m_pControl->getInputFirstName()->getPosition(), m.Middle, m.Right, m.Ctrl, m.Alt, m.Shift))
changed = true;
break;
case CRegistration::Element::InputLastName:
if (m_pControl->getInputLastName()->NotifyOnMouseLeftUp(m_pControl->FormToLocal(m.Position) - m_pControl->getInputLastName()->getPosition(), m.Middle, m.Right, m.Ctrl, m.Alt, m.Shift))
changed = true;
break;
case CRegistration::Element::Calendar:
if (m_pControl->getCalendar()->NotifyOnMouseLeftUp(m_pControl->FormToLocal(m.Position) - m_pControl->m_sCalendarRect.LeftTop, m.Middle, m.Right, m.Ctrl, m.Alt, m.Shift))
changed = true;
break;
}
if (changed)
{
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
if (m_pControl->m_pCanvasCalendar)
m_pControl->m_pCanvasCalendar->setValid(false);
m_pControl->Repaint(false);
}
}
void CRegistrationService::NotifyOnLeftMouseButtonDoubleClick(const MessageMouse &m, bool &processed)
{
CancelDown(true);
bool changed{ false };
switch (m_pControl->m_eHoveredElement)
{
case CRegistration::Element::InputFirstName:
if (m_pControl->getInputFirstName()->NotifyOnMouseLeftUp(m_pControl->FormToLocal(m.Position) - m_pControl->getInputFirstName()->getPosition(), m.Middle, m.Right, m.Ctrl, m.Alt, m.Shift))
changed = true;
break;
case CRegistration::Element::InputLastName:
if (m_pControl->getInputLastName()->NotifyOnMouseLeftUp(m_pControl->FormToLocal(m.Position) - m_pControl->getInputLastName()->getPosition(), m.Middle, m.Right, m.Ctrl, m.Alt, m.Shift))
changed = true;
break;
case CRegistration::Element::Calendar:
if (m_pControl->getCalendar()->NotifyOnMouseLeftUp(m_pControl->FormToLocal(m.Position) - m_pControl->m_sCalendarRect.LeftTop, m.Middle, m.Right, m.Ctrl, m.Alt, m.Shift))
changed = true;
break;
}
if (changed)
{
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
if (m_pControl->m_pCanvasCalendar)
m_pControl->m_pCanvasCalendar->setValid(false);
m_pControl->Repaint(false);
}
}
void CRegistrationService::NotifyOnRightMouseButtonDown(const MessageMouse &m, bool &processed)
{
CancelDown(true);
}
void CRegistrationService::NotifyOnRightMouseButtonUp(const MessageMouse &m, bool &processed)
{
CancelDown(true);
}
void CRegistrationService::NotifyOnRightMouseButtonDoubleClick(const MessageMouse &m, bool &processed)
{
CancelDown(true);
}
void CRegistrationService::NotifyOnMiddleMouseButtonDown(const MessageMouse &m, bool &processed)
{
CancelDown(true);
}
void CRegistrationService::NotifyOnMiddleMouseButtonUp(const MessageMouse &m, bool &processed)
{
CancelDown(true);
}
void CRegistrationService::NotifyOnMiddleMouseButtonDoubleClick(const MessageMouse &m, bool &processed)
{
CancelDown(true);
}
#pragma endregion
#pragma region Clipboard notifications
void CRegistrationService::NotifyOnPasteString(const MessagePasteString &m)
{
bool changed{ false };
switch (m_pControl->m_eFocusedElement)
{
case CRegistration::Element::InputFirstName:
if (m_pControl->getInputFirstName()->NotifyOnPasteString(m.Text))
changed = true;
break;
case CRegistration::Element::InputLastName:
if (m_pControl->getInputLastName()->NotifyOnPasteString(m.Text))
changed = true;
break;
}
if (changed)
{
if (m_pControl->m_pCanvas)
m_pControl->m_pCanvas->setValid(false);
m_pControl->Repaint(false);
}
}
#pragma endregion
}
}
The constructor is pretty simple. In it we just call parent class constructor and store widget class instance. We need to use widget class properties and methods in the service so we need to have pointer to widget class. In the service we almost always need to access to private properties and methods as so the service should be made a friend to widget. That is what we did earlier in widget class declaration.
In the CancelDown()
helper method we revert changes made when mouse button was down. We need to revert changes not only when button is up but in another cases as well. For example, when application is being closed or use click another mouse button and so on. So we need to do the same reverting many times and thus we created a method for it to avoid a lot of code duplications. The method also has an argument indicating whether it should also release mouse capture (we will capture mouse when left mouse button is down). We shouldn't release capture if we call this method from mouse capture release notification and should release capture in all other cases. That is what this argument for. All the method does is send cancel mouse down notification to the built-in control that was previously notified about mouse down on it. We store which built-in control is that in the m_eDownElement
member of the widget class. And in the end if release mouse capture is requested and widget captures mouse input we release it.
We do nothing special in the NotifyOnAttach()
notification. We only transfer the notification to built-in controls but we do that only when our widget is on a form. Widget can be placed on another widget and that parent widget can be not on a form. When this happens, widget still gets attach notification but in this case we better ignore it as it is often no use to do anything when widget cannot be visible. So we handle attach notification only when widget is attached to a form, directly or indirectly (attach event called whenever widget or any of its parents is attached to another widget or a form).
As mentioned earlier we need to free all platform resources we created when we receive FreeResources notification. That is what we do in the NotifyOnFreeResources()
method. We release both textures if they were created and transfer notification to built-in controls (also if they are created).
The NotifyOnTranslateChange()
just close calendar. We did it for simplicity. We could have handle possible size and position change due to new texts usage as well as handle mouse pointer change relatively to a new dimensions and position of the calendar.
In the NotifyOnParentStyleChange()
and NotifyOnStyleChange()
style change notifications we simply update widget properties from style by calling its UpdateFromStyle()
method and repaint the widget. We didn't call the Repaint()
method because we know that a form definitely need to be repainted when style is changed and thus form should call repaint itself. Also we just close the calendar instead of handling its layout change. We again did it for simplicity. We need all updates only if widget style exists. Because if style was just removed we left everything as is and no update is required.
We do the same for font change notifications NotifyOnParentFontChange()
and NotifyOnFontChange()
except for the check if font exists as the default font always exists (in case default form or default control class implementations are used; and we do use default control class implementation).
When widget gets input focus, it receives NotifyOnSetFocus()
notification which also has information about the method the widget got focus. We will handle mouse clicks later and in the NotifyOnSetFocus()
we just skip this method of getting focus. So, when our widget gets focus in the way other than by mouse click we set the property indicating which part of our widget is focused to the first name input, inform first name input about it gets focus and, just in case, inform last name input that it has no focus anymore. Finally, as usually we repaint the widget. When widget lose focus, we get NotifyOnKillFocus()
notification and we there close calendar (just in case it was opened), indicate that nothing has focus, inform both inputs they have no focus anymore and, as usual, repaint the widget.
In the Paint notification we draw widget if we got the notification in the first stage of drawing (as you might know there are two drawing stages in Nitisa) and in the second stage we draw calendar if it's opened of course.
Keyboard input notifications are all similar. Generally speaking in them we transfer keyboard input notification to corresponding focused built-in control. Some keys add special meaning to another keys if they are used together. These keys are Ctrl, Alt and Shift. Additionally there are keys which can be turned on and off and depending on it they also change the meaning of other keys. That is such keys as NumLock, CapsLock and ScrollLock. So, in order to process input successfully, built-in control need to know not only the key was down or released but also states of those special keys (at least the ones the built-in input handles). Some notifications, like some of the mouse notifications, have information about control keys, others, like keyboard notifications, don't. Fortunately there is a simple way to get information about key states. It can be done via methods of Application->Keyboard
member with is an instance of IKeyboard interface. In keyboard notifications we use it to get key information about additional key states to transfer that information to proper built-in control. When we transfer notification to a built-in control it returns true if something is changed and, thus, we need to repaint the widget. We collect that information in changed
variable and use it to check whether repainting is required or not.
Additionally in the NotifyOnKeyUp()
notification we check whether the released key is Up, Down or F4. Depending on the key we either set focus to another (previous or next) part of our widget or toggle calendar if focused part if date of birth and F4 is the key was released.
When mouse pointer moves from outside to widget we get NotifyOnMouseHover()
notification. When mouse pointer leaves widget area we get NotifyOnMouseLeave()
notification. In these methods we just update which element of a widget is under the mouse pointer. To do that we use widget class UpdateHoveredElement()
method and we need to know exact mouse pointer position in widget coordinate space for that. Mouse hover notification method has information about mouse pointer position in form coordinate space, so we change it to widget coordinate space by widget's method FormToLocal()
. The mouse leave notification has no information about mouse pointer location. Fortunately we can get it using Application->Mouse
member which is a pointer to IMouse interface. Its method getPosition()
returns mouse pointer coordinates in screen coordinate space which we should first change to form coordinate space by form's method ScreenToClient()
and then from form to widget coordinate space using again widget's method FormToLocal()
. When UpdateHoveredElement()
method changes something it means we should repaint the widget and that is what we do in the if body.
In the NotifyOnMouseMove()
notification method (which is called each time mouse pointer moves) we update hovered element at the beginning as it might change as the mouse pointer is now in a new position. Then, if mouse was down (and not yet release) over a built-in text input, we transfer notification to corresponding text input and if text input processes the notification (returns true), we set processed
also to true which is information for a form that the notification was processed by a widget and need no further processing. This flag has only meaning when mouse input capture takes place. As you will see a little bit later we capture mouse input when user downs mouse button over a built-in text input. If there is no down element we just transfer move notification to the element which under the mouse pointer now. As usually we repaint widget only when something is changed.
When user down left mouse button on the widget, it receives NotifyOnLeftMouseButtonDown()
notification. Together with left mouse button another mouse buttons and control keys may be down. We check all these in the first line of the method as we interested only in a pure left mouse button down event. Also we check there whether there is no down element yet. In case of our criteria is not met we just initiate mouse down cancel which you can see in the last line of the method. If the criteria is met, we do following. If current element under the mouse pointer is the first name built-in text input, we transfer notification to it, mark it as down element as well as focused one, inform last name text input it is not focused and inform first name input it is focused, additionally we close calendar if it is opened. We do absolutely the same if the element under the mouse pointer is the last name built-in text input. If hovered element is the calendar, we inform both built-in text inputs they are not focused, mark date of birth as focused element and transfer notification to calendar. If hovered part is the date of birth part, we inform both built-in text inputs they are not focused, mark date of birth as focused element and close calendar if it's opened (because the mouse button down was outside the calendar). If hovered part is the calendar show/hide arrow, we inform both built-in text inputs they are not focused, mark date of birth as focused element and toggle calendar visibility. In all other cases we just hide the calendar if it is shown. If any change happened we repaint the widget and (!) if down happened on any of two built-in text inputs, we capture mouse input. We need that for correct processing such features as selecting part of text by down and drag of mouse pointer. If we don't capture mouse the mouse move event won't happen if mouse pointer is outside the widget area and thus we couldn't update selection on the text in the input.
When user releases left mouse button we need to send corresponding notification to proper built-in control. If button was down over one of the built-in text inputs we need to send release notification exactly to the same text input where mouse was down independently on the current mouse position as from the down on the built-in text input that built-in text input is completely responsible for all mouse events. That is what we do in the if body at the beginning of the NotifyOnLeftMouseButtonUp()
notification method. If mouse button was not down over a text input then we just send release notification to the widget part below the mouse pointer.
In the next NotifyOnLeftMouseButtonDoubleClick()
method all we do is transmit double click notification to the built-in control currently under the mouse pointer and repaint widget if any change is happened.
In the rest mouse input notification methods we simply revert changes done when mouse left button was down (if there were any).
And the final notification in the service is NotifyOnPasteString()
. This notification happens when user attempts to paste a string from clipboard using, for example, Ctrl+V key combination. All we do in this method is transfer notification to proper text input (the text input which is now active/focused).
We now have the widget completely finished. Lets test it using the test application we created at the beginning of the tutorial. To do that add following widget class forward declaration to the FormMain.h
class right inside the nitisa
namespace.
namespace coolwidgets
{
class CRegistration;
}
After this add coolwidgets::CRegistration *m_pRegistration;
widget instance pointer declaration to the private section of the form class. Go to FormMain.cpp
and add #include "Nitisa/Core/LockRepaint.h"
to the beginning of the file and following code to the constructor body of the form.
CLockRepaint lock{ this };
m_pRegistration = new coolwidgets::CRegistration(this);
m_pRegistration->getTransform()->Translate(100, 100, 0);
Here we use repaint locking to prevent double form repainting when creating the widget (the first one would have happened when we created the widget and added it to the form and the second one would have happened when we changed widget's position). You can build and run test application and see how widget works. You can move mouse pointer over its parts to see if cursor shape really changes. You can click on first name text input or last name text input and see if proper input becomes active. You can open calendar and see how it works when you click on a date inside it or in its arrows or even somewhere else (like form's header). You even can add another widgets (for example, from Standard Package) to the form right below the registration widget and see that if you click on that widget when calendar is hidden, that widget gets your input but if calendar is shown and over that widget the calendar processes input. And so on... You may think up many of your own test scenarios for our widget.
Below you can see a screenshot of running test application.
Despite we make many simplifications (which we even didn't describe as they are not significant for tutorial project) this tutorial turned out to be quite a long. But that is not because of creating widgets is to hard. It's just because of we wrote a lot of explanations to all the parts of widgets development and partly because of we selected not the simplest widget to create. Although there are still some aspects of widget development we didn't describe here, information from this tutorial is more than enough to develop most of the widgets you may want to make.
In the next part of the tutorial series we will show how to add our Registration widget to the Form Builder.
You can find projects CoolWidgets and TestApp with lots of comments in the latest release of the Nitisa framework in Tutorials\Packages
folder of the Nitisa solution. To build it you have to select DebugRT configuration and x86 platform. To run the TestApp application from Visual Studio you need to make it StartUp Project.