Download Library BUY NOW

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.