Creating Custom Controls in SavvyUI
Modern applications demand flexible, reusable, and highly interactive user interface components. While the built-in components in SavvyUI provide a rich foundation for most application scenarios, there are times when your project requires behavior or presentation beyond what is available out of the box. In those cases, creating custom controls becomes essential.
This comprehensive guide explores how to design, implement, optimize, and maintain custom controls in SavvyUI. This article walks through the architectural concepts, lifecycle considerations, performance strategies, and best practices necessary to build scalable and maintainable SavvyUI applications.
Why Create Custom Controls?
SavvyUI includes a robust set of standard components such as buttons, grids, layouts, inputs, and containers. However, real-world applications often demand:
- Specialized visual behavior
- Composite components combining multiple controls
- Highly optimized domain-specific widgets
- Reusable UI patterns across large applications
- Custom interaction logic not supported by default components
Custom controls allow developers to encapsulate complex logic into reusable units. Instead of duplicating layout and event code throughout your application, you can centralize it within a well-structured component.
Understanding the SavvyUI Component Architecture
Before creating a custom control, it is critical to understand how SavvyUI components function. At its core, SavvyUI follows a component-based architecture where each UI element:
- Maintains its own state
- Handles lifecycle events
- Responds to user interaction
- Participates in rendering updates
Custom controls typically derive from the SavvyUI base Component class. This inheritance model allows your control to integrate seamlessly with the framework’s rendering engine, event dispatching system, and layout management.
Understanding this inheritance relationship is the first step toward building stable and predictable custom components.
Step 1: Deciding Between Building a totally new component from scratch, or composing a new component from other existing components
There are two primary approaches when building custom controls:
1. Building a totally new component from scratch
If you are developing a brand-new component in SavvyUI, consult the official documentation for complete specifications of the SavvyUI base class. You can find the full details at Base Component.
Here is an example of a custom component that handles rendering, manages its lifecycle, and processes user input:
class MyComponent: public Component
{
public:
BOOL getCreateWindowOptions(std::wstring& title, UINT& windowStyles, std::wstring& wndClassName, BOOL& isCustomWndProc) {
// Your component MUST implement the abstract getCreateWindowOptions function.
// This function should indicate whether the component uses its own custom window procedure
// or extends an existing Windows control (which uses the default Windows procedure).
// Set isCustomWndProc to TRUE if your component handles its own window messages,
// or FALSE if it relies on the default Windows control behavior.
// Specify the window styles your component requires via windowStyles.
// These must include standard Windows API styles such as WS_CHILD, WS_VISIBLE, etc.
// If extending an existing Windows control, set wndClassName to the existing control's class name,
// e.g., "BUTTON", "EDIT", etc.
// If not extending an existing control, provide a unique class name to avoid conflicts,
// ideally prefixed with your company or project name, e.g., "MyCompanyChartComponent".
// Return TRUE to allow SavvyUI to create the component.
// Example implementation:
isCustomWndProc = TRUE; // Custom rendering and message handling
wndClassName = L"MyCompanyChartComponent";
windowStyles = WS_CHILD | WS_VISIBLE; // Add other styles as needed
return TRUE;
}
void windowCreated() {
// Your component MUST implement the abstract windowCreated function.
// This function is called immediately after SavvyUI creates your component.
// Use it to perform initialization tasks, such as loading data or creating child components.
// At this stage, your component already has a valid window handle.
}
int getMinimumHeight () {
// OPTIONAL
// Return the minimum height in pixels required to display your component,
// or return -1 if no specific minimum height is enforced.
// If your component does not enforce a minimum height, DO NOT implement this optional function
}
int getPreferredHeight() {
// OPTIONAL
// Return the preferred default height in pixels for your component,
// or -1 if no specific preferred height is set.
// If your component does not enforce a preferred height, DO NOT implement this optional function
}
BOOL getChildren (vector< Component * > &children) {
// OPTIONAL unless your component contains other components.
// If your component includes other components, fill the 'children' vector
// with the child components that your custom component manages.
// If your component does not include other components, DO NOT implement this optional function
}
void onPaint(Graphics* g) {
// Required when your component is not extending an existing windows control.
// Implement the rendering logic for your custom component here.
// Use the provided Graphics object to draw text, shapes, images, and other visual elements.
}
void onWindowResized() {
// This function is called whenever the component's size changes.
// If the component has child components, adjust their positions and sizes as needed.
// Finally, call repaint() to refresh the component's display, even if there are no child components.
}
// The following functions are optional; you can implement any of these event handlers that your component needs to respond to.
void onMousePressed (WinHandle hWnd, int x, int y, int clickCount, BOOL shiftPressed, BOOL ctrlPressed)
Called when the mouse is pressed within the component.
void onMouseReleased (WinHandle hWnd, int x, int y, BOOL shiftPressed, BOOL ctrlPressed)
Called when the mouse button is released within the component.
void onMouseRightClicked (WinHandle hWnd, int x, int y, BOOL shiftPressed, BOOL ctrlPressed)
Called when the right mouse button is clicked within the component.
void onMouseMoved (WinHandle hWnd, int x, int y, BOOL shiftPressed, BOOL ctrlPressed)
Called when the mouse is moved within the component.
BOOL onMouseWheel (WinHandle hWnd, int x, int y, int delta)
Called when the mouse wheel is used over the component.
void onArrowLeft (BOOL shiftPressed, BOOL ctrlPressed)
void onArrowRight (BOOL shiftPressed, BOOL ctrlPressed)
void onArrowUp (BOOL shiftPressed, BOOL ctrlPressed)
void onArrowDown (BOOL shiftPressed, BOOL ctrlPressed)
void onPageUp (BOOL shiftPressed, BOOL ctrlPressed)
void onPageDown (BOOL shiftPressed, BOOL ctrlPressed)
void onKeyHome (BOOL shiftPressed, BOOL ctrlPressed)
void onKeyEnd (BOOL shiftPressed, BOOL ctrlPressed)
void onKeyInsert (BOOL shiftPressed, BOOL ctrlPressed)
void onKeyDelete (BOOL shiftPressed, BOOL ctrlPressed)
void onKeyBackSpace (BOOL shiftPressed, BOOL ctrlPressed)
void onKeyTyped (wchar_t ch, BOOL shiftPressed, BOOL ctrlPressed)
void onKeyTab (BOOL shiftPressed, BOOL ctrlPressed)
void onKeyEnter (BOOL shiftPressed, BOOL ctrlPressed)
void onKeyF1 (BOOL shiftPressed, BOOL ctrlPressed)
void onKeyF2 (BOOL shiftPressed, BOOL ctrlPressed)
void onKeyF3 (BOOL shiftPressed, BOOL ctrlPressed)
void onKeyF4 (BOOL shiftPressed, BOOL ctrlPressed)
void onKeyF5 (BOOL shiftPressed, BOOL ctrlPressed)
void onKeyF6 (BOOL shiftPressed, BOOL ctrlPressed)
void onKeyF7 (BOOL shiftPressed, BOOL ctrlPressed)
void onKeyF8 (BOOL shiftPressed, BOOL ctrlPressed)
void onKeyF9 (BOOL shiftPressed, BOOL ctrlPressed)
void onKeyF10 (BOOL shiftPressed, BOOL ctrlPressed)
void onKeyF11 (BOOL shiftPressed, BOOL ctrlPressed)
void onKeyF12 (BOOL shiftPressed, BOOL ctrlPressed)
void onFocusGained ()
Called when the component gains keyboard focus.
void onFocusLost ()
Called when the component loses keyboard focus.
void onTimer (unsigned int timerId)
Called on timer events.
protected:
// The following functions are utility methods provided by the Base Component class, enabling your custom component to trigger different types of events.
void protectedAddDataChangedListener (DataChangeListener *l)
Adds a data change listener.
void protectedAddSelectionChangedListener (SelectionChangeListener *l)
Adds a selection change listener.
void protectedAddActionListener (ActionListener *l)
Adds an action listener.
void protectedAddItemDoubleClickedListener (RowDoubleClickListener *l)
Adds a row double-click listener.
void protectedFireDataChangedEvent (const wstring &oldValue, const wstring &newValue)
Fires a data changed event to registered listeners.
void protectedFireSelectionChangedEvent (long selectionIndex=-1, const wstring &selectionValue=L"", BOOL checked=FALSE)
Fires a selection changed event to registered listeners.
void protectedFireActionEvent (long actionId=-1, const wstring &actionName=L"")
Fires an action event to registered listeners.
void protectedFireItemDoubleClickedEvent (__int64 rowIndex)
Fires a row double-click event to registered listeners.
};
2. Composing a new component from other existing components
If your control combines multiple elements (e.g., label + input + validation + icon), composition is often the better choice. In this approach, your custom component internally manages several child components and coordinates their behavior.
class PasswordEntryForm : public GridPanel, public ActionListener
{
Label _usernameLabel, _passwordLabel;
TextField _usernameField;
PasswordField _passwordField;
Button _okBtn, _cancelBtn;
public:
PasswordEntryForm()
{
_usernameLabel.setText(L"Username:");
_passwordLabel.setText(L"Password:");
_okButton.setText(L"OK");
_okButton.addActionListener(this);
_cancelBtn.setText(L"Cancel");
_cancelBtn.addActionListener(this);
// Add the components to the screen
setLayout({ 130, -1, 80, 80 }, { 30, 30, 10, 30 });
addComponent(&_usernameLabel, 0, 0);
addComponent(&_usernameField, 1, 0, 3);
addComponent(&_passwordLabel, 0, 1);
addComponent(&_passwordField, 1, 1, 3);
addComponent(&_okButton, 2, 2);
addComponent(&_cancelButton, 3, 2);
}
void onAction(const ActionEvent& ev)
{
if(ev.sourceComponentId == _okBtn.getId())
{
wstring userName = _usernameField.getText();
...
}
else if(ev.sourceComponentId == _cancelBtn.getId())
{
}
}
};
Step 2: Defining the Control’s Public Interface
Every custom control should expose a clear and minimal public API. This includes:
- Properties
- Events
- Methods
Avoid exposing unnecessary internal state. A well-designed interface ensures that:
- The control is easy to reuse
- Future refactoring does not break consumers
- The component remains modular
Think of your custom control as a reusable product. Other developers (or even future you) should be able to integrate it without understanding its internal mechanics.
Step 3: Managing the Component Lifecycle
SavvyUI components follow a predictable lifecycle. When building custom controls, you must carefully manage:
- Initialization logic
- Rendering behavior
- Event handling
- Cleanup and destruction
Initialization
During construction or initialization, allocate only the resources necessary for the control’s operation. Avoid heavy computations in constructors.
Post-Creation and Rendering
After the control is created, ensure that all child components are correctly initialized.
Destruction and Cleanup
If your control allocates dynamic memory , ensure that cleanup occurs when the component is destroyed. Memory leaks in custom controls can degrade performance across the entire application.
Step 4: Rendering Efficiency
Rendering performance is critical when creating custom controls. Poorly designed components can trigger unnecessary re-renders and increase CPU usage.
To optimize rendering:
- Minimize state changes
- Avoid redundant computations during updates
- Batch updates where possible
- Prevent unnecessary child re-renders
SavvyUI’s built-in components are optimized for efficient rendering. Your custom controls should follow similar design principles.
Step 5: Handling Events Properly
Event management is a core aspect of interactive controls. Whether handling clicks, or keyboard input, your component should:
- Implement only necessary event functions
Improper event handling can lead to “ghost events” or performance issues due to duplicated listeners.
Step 6: Styling and Theming
A custom control should integrate naturally into the application’s design system.
This ensures your component remains flexible and reusable across multiple projects.
Step 7: Performance Considerations
When creating advanced controls such as charts, dashboards, or large data visualizations, performance becomes even more important.
Memory Management
If your control dynamically allocates resources, ensure they are properly released. Avoid unnecessary pointer usage unless ownership is clearly defined.
Data Optimization
If your component handles large datasets, implement paging to prevent excessive memory usage.
Threading for Heavy Tasks
If your control performs computationally intensive operations, consider running them in background threads to avoid blocking the main UI thread.
Step 8: Reusability and Maintainability
Reusable custom controls reduce duplication and improve long-term maintainability. To achieve this:
- Keep components single-purpose
- Document public APIs
- Write clear and modular logic
- Avoid tightly coupling with global state
Well-designed components become building blocks for entire application ecosystems.
Testing Custom Controls
Testing ensures that your custom controls behave consistently. Consider:
- Unit testing component logic
- Integration testing within layouts
- Stress testing under heavy data loads
Testing is especially important when controls handle memory allocation or complex rendering logic.
Common Pitfalls to Avoid
- Overcomplicating the component design
- Failing to clean up resources
- Triggering excessive re-renders
- Hardcoding styles
- Ignoring scalability
Avoiding these mistakes will save significant refactoring time later.
Example Use Cases for Custom Controls
Some practical examples where custom controls shine include:
- Advanced data grids with inline editing
- Interactive financial dashboards
- Custom form validation components
- Dynamic navigation panels
- Reusable modal systems
Designing for Scalability
As your application grows, your custom controls must scale with it. Plan for:
- Future feature expansion
- Performance under heavy load
- Cross-team collaboration
Modular architecture and consistent coding standards make scaling significantly easier.
Conclusion
Creating custom controls in SavvyUI empowers developers to build highly interactive, reusable, and scalable user interfaces tailored to specific application needs. By understanding component architecture, lifecycle management, rendering optimization, memory handling, and performance best practices, you can design controls that are both powerful and efficient.
As you continue developing advanced applications with SavvyUI, investing time in building well-structured custom controls will pay long-term dividends in maintainability, performance, and development speed.
To learn more about SavvyUI and explore additional resources, visit the official homepage at https://www.savvyui.com.