Displaying properties using selection and events
Up to this point, we’ve learned how to load an IFC file, read the metadata to get IFC properties, and then use some of the property values to control how the model is displayed. Building on top of that knowledge, we’re now going to extend the sandbox to display component properties whenever a user highlights an object. In doing so, we’ll learn how to make use of the HOOPS Visualize Event classes.
One difference to note between what we’ve done so far and this new task, is that we’ll be updating the user interface based on function calls from HOOPS Visualize. Previously, both on loading the model and displaying properties, we’ve called Visualize functionality from the application user interface. As both Windows and HOOPS Visualize make use of multiple threads, as we’ll see later, we need to account for this in our use of Visualize and MFC.
The objective of this tutorial section is create an application pane in which the properties associated with a highlighted component can be displayed. It will be possible to highlight the component either through the model browser or using a selection operator. The component properties will be displayed using an MFC control of type CMFCPropertyGridCtrl
. This will be embedded in a dockable pane of type CDockablePane
.
This is what we’re going to build for this tutorial task:
The steps required to implement this are:
Coding of the pane and its child property grid
Integration of the pane into the user interface framework
Definition of an event handler to react to a change in highlighted component
Update of the property grid when a new component is highlighted
Creating the component properties pane
For this tutorial we created a very basic dockable pane and child properties grid. The code to create the pane and child grid is shown below. You will see a reference to a custom event handler which we will discuss later.
class CHPSComponentPropertiesPane : public CDockablePane
{
public:
CHPSComponentPropertiesPane();
virtual ~CHPSComponentPropertiesPane();
void SetComponentProperties(HPS::Component &component);
void Flush();
void AdjustLayout();
protected:
CMFCPropertyGridCtrl _propertyCtrl;
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg void OnPaint();
afx_msg LRESULT OnComponentHighlighted(WPARAM w, LPARAM l);
DECLARE_MESSAGE_MAP()
private:
void UpdateCanvas();
private:
IFCHighlightHandler _highlightHandler;
};
Most of the functionality resides in the function SetComponentProperties
, which fills out the properties grid. Here you will see that we make use of previous functionality to obtain a list of component properties. One thing to note is that you need to create a user defined resource ID in the Create
function.
CHPSComponentPropertiesPane::CHPSComponentPropertiesPane()
{
}
CHPSComponentPropertiesPane::~CHPSComponentPropertiesPane()
{
}
BEGIN_MESSAGE_MAP(CHPSComponentPropertiesPane, CDockablePane)
ON_WM_CREATE()
ON_WM_SIZE()
ON_WM_PAINT()
ON_MESSAGE(WM_MFC_SANDBOX_COMPONENT_HIGHLIGHT, &OnComponentHighlighted)
END_MESSAGE_MAP()
int CHPSComponentPropertiesPane::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CDockablePane::OnCreate(lpCreateStruct) == -1)
return -1;
CRect rectDummy;
rectDummy.SetRectEmpty();
if (!_propertyCtrl.Create(WS_VISIBLE | WS_CHILD, rectDummy, this, ID_COMPONENTPROPS_CTRL))
{
TRACE0("Failed to create properties window \n");
return -1; // fail to create
}
_propertyCtrl.EnableHeaderCtrl(TRUE, _T("Property"), _T("Value"));
_propertyCtrl.SetVSDotNetLook();
//! [IFCHighlightHandler::Register]
HPS::EventDispatcher dispatcher = HPS::Database::GetEventDispatcher();
_highlightHandler._parentPane = this;
dispatcher.Subscribe(_highlightHandler, HPS::Object::ClassID<HPS::ComponentHighlightEvent>());
//! [IFCHighlightHandler::Register]
return 0;
}
afx_msg LRESULT CHPSComponentPropertiesPane::OnComponentHighlighted(WPARAM w, LPARAM l)
{
SetComponentProperties( _highlightHandler._currentComponent);
return 0;
}
void CHPSComponentPropertiesPane::OnSize(UINT nType, int cx, int cy)
{
CDockablePane::OnSize(nType, cx, cy);
AdjustLayout();
}
void CHPSComponentPropertiesPane::AdjustLayout()
{
if (GetSafeHwnd() == NULL || (AfxGetMainWnd() != NULL && AfxGetMainWnd()->IsIconic()))
return;
CRect rectClient;
GetClientRect(rectClient);
_propertyCtrl.SetWindowPos(NULL, rectClient.left + 3, rectClient.top + 2, rectClient.Width() - 4, rectClient.Height() - 7, SWP_NOACTIVATE | SWP_NOZORDER);
}
void CHPSComponentPropertiesPane::OnPaint()
{
CPaintDC dc(this); // device context for painting
CRect paneRect;
GetWindowRect(paneRect);
ScreenToClient(paneRect);
CBrush bg;
bg.CreateStockObject(WHITE_BRUSH);
dc.FillRect(&paneRect, &bg);
CRect rectPropertyCtrl;
_propertyCtrl.GetWindowRect(rectPropertyCtrl);
ScreenToClient(rectPropertyCtrl);
rectPropertyCtrl.InflateRect(1, 1);
dc.Draw3dRect(rectPropertyCtrl, ::GetSysColor(COLOR_3DSHADOW), ::GetSysColor(COLOR_3DSHADOW));
}
void CHPSComponentPropertiesPane::Flush()
{
SetWindowText(_T("Component Properties"));
_propertyCtrl.RemoveAll();
_propertyCtrl.AdjustLayout();
}
void CHPSComponentPropertiesPane::SetComponentProperties(HPS::Component &component)
{
_propertyCtrl.RemoveAll();
std::vector< std::pair<HPS::UTF8, HPS::UTF8>> properties;
GetComponentMetaData(component, properties);
for (auto const &keyValue : properties)
{
CString TextName(keyValue.first);
CString TextValue(keyValue.second);
CMFCPropertyGridProperty* pTextVal = new CMFCPropertyGridProperty(TextName, TextValue);
_propertyCtrl.AddProperty(pTextVal);
}
_propertyCtrl.AdjustLayout();
_propertyCtrl.RedrawWindow();
}
void CHPSComponentPropertiesPane::UpdateCanvas()
{
CHPSDoc * doc = GetCHPSDoc();
HPS::Canvas canvas = doc->GetCHPSView()->GetCanvas();
canvas.Update();
}
Integrating the pane into the frame
Perhaps the most time consuming part of this exercise is to integrate the pane into the existing user interface. The integration needs to take place in the class CHPSFrame
. You can use references to the existing properties pane as a guide. The existing properties pane is used only for the segment browser (not the model browser), and thus deals with HPS::Segment
properties, not HPS::Component
properties. Fortunately, what we’re doing for this tutorial is less complex.
Here are the general steps required to implement this in CHPSFrame
:
Define a member variable of type
CHPSComponentPropertiesPane
in CHPSFrame.hDeclare functions to get the handle to the pane and control visibility
Create and initialize the pane in the frame
protected:
CHPSComponentPropertiesPane m_componentPropertiesPane;
Add the following code to the OnCreate()
function in CHPSFrame.cpp to create the pane and enable docking.
if (!m_componentPropertiesPane.Create(_T("Component Properties"), this, CRect(0, 0, 250, 250), TRUE, ID_PANE_COMPONENT_PROPS, dwStyle | CBRS_LEFT))
{
TRACE0("Failed to create Component Properties pane\n");
return FALSE; // failed to create
}
// ...
m_componentPropertiesPane.EnableDocking(CBRS_ALIGN_ANY);
m_componentPropertiesPane.DockToWindow(&m_tabbedPane, CBRS_ALIGN_LEFT);
m_componentPropertiesPane.ShowPane(FALSE, FALSE, FALSE);
Note that the pane will not initially be displayed. You can either enable visibility at this point in the initialization process, or add a control button to the user interface to display it.
Adding a custom HOOPS Visualize event handler
We want the contents of the property grid to be updated whenever the user highlights an object. To do this, we’re going to use a custom event handler. For more background on event handling, please refer to the event handling section of the HOOPS Visualize documentation.
In the following code sample, we define a custom ComponentHighlightEvent
handler which is associated with the property pane we just created:
HPS::EventHandler::HandleResult IFCHighlightHandler::Handle(HPS::Event const * pInEvent)
{
HPS::ComponentHighlightEvent * pCHEvent = (HPS::ComponentHighlightEvent *)pInEvent;
if (!pCHEvent->path.Empty())
{
if (NULL != _parentPane)
{
_currentComponent = pCHEvent->path.Front();
_parentPane->PostMessageW(WM_MFC_SANDBOX_COMPONENT_HIGHLIGHT, 0, 0);
}
}
return HandleResult::NotHandled;
}
This code will save a reference to the current highlighted component, then will generate a custom Windows message which it dispatches to the owning pane. The reason we do this is so that the pane can respond to the Windows message coming from the MFC thread and update itself. We cannot call the pane directly from the HOOPS Visualize thread handling the HPS events.
We need to do a little more work in order to make this happen.
Adding a custom Windows message handler
The first page of creating a custom message handler is to first define a custom Windows message for use by our application. The MFC Sandbox already makes use of some and you can add another. They are defined in the file CHPSApp.h. Just add a line of code similar to this:
#define WM_MFC_SANDBOX_COMPONENT_HIGHLIGHT (WM_USER + 104)
You’ll notice when the customer event handler posts a message, it sends it to the custom pane we defined. Therefore, we next need to define a Windows message handler to respond to this message. The declaration for CHPSComponentPropertiesPane
already does this. Look for the following line:
afx_msg LRESULT OnComponentHighlighted(WPARAM w, LPARAM l);
This is called in the message map. Its implementation is shown earlier.