Overslaan naar inhoud
CTech Digital
  • Startpagina
  • Odoo services
  • AI services
  • Contact
  • 0
  • Nederlands (BE) English (UK) Français
CTech Digital
  • 0
    • Startpagina
    • Odoo services
    • AI services
    • Contact
  • Nederlands (BE) English (UK) Français

Observer pattern

  • Alle blogs
  • Daily blog
  • Observer pattern
  • 31 oktober 2025 in
    CTech Metrology, Luc Wens

    Introduction

    The observer pattern manages a group of views that need to be updated when a subject changes.

    This article is written at the end of making a test program which can be found on Github

    https://github.com/lucwens/ObserverPattern

    The views can register themselves in an ObserverManager, once they are derived from Observer. 

    Observed subjects to not need to be derived from a base class, but they do need to instrument the functions that change their structure.

    The obserserver manager can manage multiple data sources, for example, when looking at CTrack:

    • Node changes : 

      • add/delete nodes
      • change the name
      • change the color
      • change the position due to a new measurement
    • Status changes
    • Engine connection changes
    • License changes
    • Cursor updates

    We have one manager that manages everything, but we introduce change categories for the different groups. 

    So we have change categories, each category can have its own sets of changes. Views can subscribe to one or more change categories.

    Let's look at a change event for node changes

    // Windows messages for category-specific change processing
    #define WM_PROCESS_NODE_CHANGES (WM_USER + 1)
    #define WM_PROCESS_STATUS_CHANGES (WM_USER + 2)
    #define WM_PROCESS_CONNECTION_CHANGES (WM_USER + 3)

    // Enum for different categories of changes

    enum class ChangeCategory
    {
    Node,
    Status,
    Connection
    };

    // Enum for different types of node changes

    enum class NodeChangeType
    {
    NodeAdded,
    NodeRemoved,
    NodeModified,
    NodeParentChanged,
    NodeChildAdded,
    NodeChildRemoved,
    BatchUpdateBegin,
    BatchUpdateEnd,
    NodeSelected, // New event for node selection
    NodeColorChanged,
    NodeChangeType_New = 100, // Explicit rebuild trigger
    NodeChangeType_OpenFromFile = 101 // Explicit rebuild trigger
    };

    // Structure to hold information about a node change

    struct NodeChange
    {
    NodeChangeType type;
    NodeId nodeId;
    NodeId relatedNodeId; // For parent/child changes

    NodeChange(NodeChangeType t, NodeId id, NodeId relatedId = 0) : type(t), nodeId(id), relatedNodeId(relatedId) {}

    };

    All the change categories are grouped in ChangeCategory. For every category there is one WM_USER message. This guarantees worker thread isolation, we don't want worker thread to call main UI functions for updating views, the only way to do this is by having the worker thread post messages to the main UI window, typically the mainframe, there the changes can be handled by looping over the observers.

    For every change category we have

    • A change type enum: reflects the action on the item
    • A change structure with the change type and extra data fields
    • A WM_USER message that triggers the view updates

    When introducing a new change category, ChangeTypes.h needs to be updated.

    All types of changes are grouped in ChangeEvent which is a std::variant that contains all possible changes. Supporting routines for ChangeEvent:

    • GetChangeCategory : returns the change category for a ChangeEvent
    • GetWindowsMessage : returns the WM_USER message for that ChangeEvent
    • IsRebuildChange : returns true when the complete view needs to be rebuild
    • GetChangeDescription : for logging/debugging 

    View update efficiency

    Especialy the 3D view update when doing measurements can be tricky because you don't want to recreate the complete 3D scene, but you also don't want to do an update if you have mulitple items entering the scene.

    There are 2 ways to update the view

    • Do a redraw each time something changes
    • Collect a number of changes and do only one redraw : batch updating

    The second is more efficient when multiple compound changes happen, the redraw of the screen will happen only once.

    And then there are also 2 ways of refreshing the view

    • Redraw the window to reflect the updated changes
    • Rebuild the complete view

    The latter will be done when opening a project and basically everything changes.

    When there is one change event that requires a rebuild, then all other redraws can be discarded.

    Instrumenting the observed subject

    Let's take the example of the node factory.

    When a new node is created we call CreateNode, this can be in a worker thread.

    Node *NodeFactory::CreateNode(const std::string &name, NodeId parentId)
    {
    PROFILE_SCOPE("NodeFactory::CreateNode", TracyColor);

    // Can't use std::make_unique with private constructor

    // Using new directly since NodeFactory is a friend class
    std::unique_ptr<Node> node(new Node());
    node->m_pFactory = this;
    node->SetName(name);

    NodeId id = node->GetId();

    Node *nodePtr = node.get();

    // Set parent if specified

    if (parentId != 0)
    {
    Node *parent = FindNodeById(parentId);
    if (parent)
    {
    node->SetParentId(parentId);
    parent->AddChildId(id);
    }
    }

    m_nodes[id] = std::move(node);


    // Notify about the new node

    NotifyChange(NodeChange(NodeChangeType::NodeAdded, id));

    // Notify about parent-child relationship if applicable

    if (parentId != 0)
    {
    NotifyChange(NodeChange(NodeChangeType::NodeChildAdded, parentId, id));
    }

    return nodePtr;

    }

    The ProfileScope is a tracy profiler instrumentation so we can follow and time this action.

    In the first part of the code the actual action is done.

    At the bottom we have the change notification, done by the member function NotifyChange.

    void NodeFactory::NotifyChange(const NodeChange &change)
    {
    PROFILE_SCOPE("NodeFactory::NotifyChange", TracyColor);

    if (m_isShuttingDown)

    return;

    if (IsInBatchUpdate())

    {
    m_pendingChanges.push_back(change);
    return;
    }

    // Delegate to ObserverManager for thread-safe notifications

    // This ensures all notifications happen on the main thread via Windows messages
    ObserverManager::Instance().NotifyChange(change);
    }

    When batch mode is active, we do not signal the WM_USER message, but rather collect the changes into an internal FIFO buffer 



    in Daily blog
    # CTrack
    Reworking measurements of geometries
    Copyright © CTech
    Nederlands (BE) | English (UK) | Français
    Aangeboden door Odoo - De #1 Open source e-commerce