If you have followed Part 1 of this tutorial, you should have a working panel background. Time to add something useful ...
Lesson 5: Adding an MFD
Multifunctional displays (MFD) are the primary user interface for most Orbiter spacecraft, and consequently they feature in most instrument panels. So let’s start our panel instrumentation by adding an MFD.
MFDs consist of two main components: a square display, and a set of control buttons around it. The display will have its own texture, supplied by Orbiter. All we have to do is provide a mesh group that references this texture. (A separate group is required because each mesh group can only use a single texture, and our main panel group already uses the 2-D panel texture.) Since the display is square, it can be defined by 4 vertices and 2 triangles. The following code is appended to the DefineMainPanel method to define the MFD display group:
Note the texture coordinates in the MFD vertex definition which associates the bottom left corner of the display with texture coordinates (0,0), and the top right corner with coordinates (1,1). This makes sure that the display is properly rendered.
The call to RegisterPanelMFDGeometry registers the newly created mesh group (group 1 of mesh 0) as the display for the left MFD.
Important: Since the MFD display overlaps the panel background, it is important that the groups are rendered in the correct order to make sure that the MFD is displayed on top of the background panel, rather than the other way round. The panel mesh groups are rendered in the order they appear in the mesh, so the MFD display group must be added after the panel background group.
We have the display now, but no means of manipulating it. For that, we need to define the MFD buttons. They are slightly more intricate to code for two reasons: they must respond to mouse events, and they must be able to update the labels displayed on them.
There are two techniques for dynamically updating the labels: (a) draw the label text using Windows GDI functions, and (b) blit the label letter by letter from a bitmap containing the full character set. The problem with method (a) is that we can’t use GDI drawing directly on the panel background texture, because many graphics drivers don’t allow GDI drawing on compressed and/or texture surfaces. So we would have to create a separate uncompressed surface, draw the labels on it, and then blit the result into the panel background texture. Method (b) avoids GDI drawing altogether and is therefore probably more robust, but it requires some additional code support.
Here is an outline of how to implement method (b). Note that this technique can be applied not only to MFD button labels, but to any dynamically updated panel text.
First you need to create a bitmap containing a full character set of all characters that may appear in an MFD label (i.e. all printable ASCII characters). You can even use the same texture that contains the panel background for this. Draw the characters in the required size and font. Next, you need to create lists of character position and width in your code, similar to this:
which assumes that all characters are written on one line (i.e. have the same y-position) at the top edge of the texture (MFD_font_ypos = 0). Adjust the parameters as required. Note that you can simplify this process significantly if you are using a fixed-width font (all character widths are the same, and the x-position can be inferred from the character ASCII value if they appear in order, so both lists can be omitted).
Now that the label font is defined, we can go about implementing the buttons. For defining the function button columns to the left and right of the MFD display, I am implementing a new class MFDButtonCol, derived from the PanelElement class found in Samples\Common\Vessel\Instrument.h (*). (If you want to use PanelElement, you need to include Samples\Common\Vessel\Instrument.cpp in your project.) The class interface looks like this:
The PanelElement class provides a common framework for active panel elements to update themselves and react to mouse events. It also provides methods for VC interaction, but here we are only concerned about 2-D panel functions.
Parameter lr identifies the left (0) or right (1) button column of the MFD. The implementation might look like this:
The Redraw2D method loops over the 6 buttons controlled by the object, blanks each by blitting the button background into the panel (blank_btn_x and blank_btn_y are assumed to point to a region in the texture containing the button background), and then blits the characters of the new label into place.
Finally, we also need to implement the mouse event handler:
Now all that remains is to create the required button column objects in the MyVessel class, and to bind them into the appropriate callback functions.
pel is a list of all the objects making up the active elements of our panel. Currently this contains only two objects (the left and right button columns of the MFD), but we will extend this later. We can now instantiate the panel elements in the vessel constructor:
and delete them in the destructor:
Inside the DefineMainPanel method, we register the button columns as active regions:
where AID_MFD_LBUTTONS and AID_MFD_RBUTTONS are region identifiers, and x0, x1, x2, x3, y0, y1 are the pixel coordinates for the button column regions in the texture.
We also need to implement the clbkPanelMouseEvent and clbkPanelRedrawEvent callback functions, but they have a simple form thanks to the common interface of the PanelElement class:
Finally, we have to make sure that a redraw event is triggered for the MFD buttons whenever the labels change. The best way to catch this event is in the clbkMFDMode callback function, which is called whenever the MFD mode (and hence the labels) change.
And that’s all there is to it (not even close to rocket science)! You now have a panel with a working MFD display and function buttons. (I have omitted the implementation of the 3 control buttons at the bottom edge, but this follows a similar pattern and is in fact simpler, because the labels are static. The implementation is left as an exercise for the intrepid developer.)
Compile your code and try out your panel. If everything works, you can implement the right MFD in the same way.
Part 3 of the tutorial adds a switch ...
(*) Note that Instrument.h and Instrument.cpp are not yet located in Samples\Common\Vessel as of Orbiter100830 (they will be moved there in the next beta). For now, you can find them in Samples\DeltaGlider.
Lesson 5: Adding an MFD
Multifunctional displays (MFD) are the primary user interface for most Orbiter spacecraft, and consequently they feature in most instrument panels. So let’s start our panel instrumentation by adding an MFD.
MFDs consist of two main components: a square display, and a set of control buttons around it. The display will have its own texture, supplied by Orbiter. All we have to do is provide a mesh group that references this texture. (A separate group is required because each mesh group can only use a single texture, and our main panel group already uses the 2-D panel texture.) Since the display is square, it can be defined by 4 vertices and 2 triangles. The following code is appended to the DefineMainPanel method to define the MFD display group:
Code:
void MyVessel::DefineMainPanel (PANELHANDLE hPanel)
{
...
static NTVERTEX VTX_MFD[4] = {
{100, 50,0, 0,0,0, 0,0},
{400, 50,0, 0,0,0, 1,0},
{100,350,0, 0,0,0, 0,1},
{400,350,0, 0,0,0, 1,1}
};
static WORD IDX_MFD[6] = {
0,1,2,
3,2,1
};
MESHGROUP grp_mfd = {VTX_MFD, IDX_MFD, 4, 6, 0, 0, 0, 0, 0};
oapiAddMeshGroup (hPanelMesh, &grp_mfd);
RegisterPanelMFDGeometry (hPanel, MFD_LEFT, 0, 1);
}
The call to RegisterPanelMFDGeometry registers the newly created mesh group (group 1 of mesh 0) as the display for the left MFD.
Important: Since the MFD display overlaps the panel background, it is important that the groups are rendered in the correct order to make sure that the MFD is displayed on top of the background panel, rather than the other way round. The panel mesh groups are rendered in the order they appear in the mesh, so the MFD display group must be added after the panel background group.
We have the display now, but no means of manipulating it. For that, we need to define the MFD buttons. They are slightly more intricate to code for two reasons: they must respond to mouse events, and they must be able to update the labels displayed on them.
There are two techniques for dynamically updating the labels: (a) draw the label text using Windows GDI functions, and (b) blit the label letter by letter from a bitmap containing the full character set. The problem with method (a) is that we can’t use GDI drawing directly on the panel background texture, because many graphics drivers don’t allow GDI drawing on compressed and/or texture surfaces. So we would have to create a separate uncompressed surface, draw the labels on it, and then blit the result into the panel background texture. Method (b) avoids GDI drawing altogether and is therefore probably more robust, but it requires some additional code support.
Here is an outline of how to implement method (b). Note that this technique can be applied not only to MFD button labels, but to any dynamically updated panel text.
First you need to create a bitmap containing a full character set of all characters that may appear in an MFD label (i.e. all printable ASCII characters). You can even use the same texture that contains the panel background for this. Draw the characters in the required size and font. Next, you need to create lists of character position and width in your code, similar to this:
Code:
const int MFD_font_xpos[256] = {
0,0, ..., 100/*A*/, 110/*B*/, 118/*C*/, ...
};
const int MFD_font_width[256] = {
0,0, ..., 9/*A*/, 7/*B*/, 7/*C*/, ...
};
const int MFD_font_ypos = 0;
const int MFD_font_height = 16;
Now that the label font is defined, we can go about implementing the buttons. For defining the function button columns to the left and right of the MFD display, I am implementing a new class MFDButtonCol, derived from the PanelElement class found in Samples\Common\Vessel\Instrument.h (*). (If you want to use PanelElement, you need to include Samples\Common\Vessel\Instrument.cpp in your project.) The class interface looks like this:
Code:
#include “..\Common\Vessel\Instrument.h”
class MFDButtonCol: public PanelElement {
public:
MFDButtonCol (VESSEL3 *v, DWORD _lr);
bool Redraw2D (SURFHANDLE surf);
bool ProcessMouse2D (int event, int mx, int my);
private:
DWORD lr; // left (0) or right (1) button column
DWORD xcnt; // central axis of button column in texture
DWORD ytop; // top edge of label in topmost button
DWORD dy; // vertical button spacing
};
Parameter lr identifies the left (0) or right (1) button column of the MFD. The implementation might look like this:
Code:
MFDButtonCol::MFDButtonCol (VESSEL3 *v, DWORD _lr)
: PanelElement (v)
{
lr = _lr;
xcnt = 40 + lr*380; // adjust according to geometry
ytop = 300; // same here
dy = 38; // same here
}
bool MFDButtonCol::Redraw2D (SURFHANDLE surf)
{
const int btnw = 16; // button label area width
const int btnh = MFD_font_height; // button label area height
int btn, x, len, i, w;
const char *label;
for (btn = 0; btn < 6; btn++) {
// blank buttons
oapiBlt (surf, surf, xcnt-btnw/2, ytop+dy*btn,
blank_btn_x, blank_btn_y, blank_btn_x+btnw, blank_btn_y+btnh);
// write labels
if (label = oapiMFDButtonLabel (MFD_LEFT, btn+lr*6)) {
len = strlen(label);
for (w = i = 0; i < len; i++)
w += MFD_font_width[label[i]];
for (i = 0, x = xcnt-w/2; i < len; i++) {
w = MFD_font_width[label[i]];
if (w) {
oapiBlt (surf, surf, x, ytop+dy*btn, MFD_font_xpos[label[i]],
MFD_font_ypos, w, MFD_font_height);
x += w;
}
}
} else break;
}
return false;
}
Finally, we also need to implement the mouse event handler:
Code:
bool MFDButtonCol::ProcessMouse2D (int event, int mx, int my)
{
if (my % dy < button_height) {
int btn = my/dy + lr*6;
oapiProcessMFDButton (MFD_LEFT, btn, event);
return true;
} else
return false;
}
Code:
class MyVessel: public VESSEL3 {
...
PanelElement *pel[2];
...
};
Code:
MyVessel::MyVessel (...)
{
...
for (int i = 0; i < 2; i++)
pel[i] = new MFDButtonCol (this, i);
...
}
Code:
MyVessel::~MyVessel ()
{
...
for (int i = 0; i < 2; i++)
delete pel[i];
...
}
Code:
void MyVessel::DefineMainPanel ()
{
...
RegisterPanelArea (hPanel, AID_MFD_LBUTTONS, _R(x0,y0,x1,y1),
PANEL_REDRAW_USER,
PANEL_MOUSE_LBDOWN|PANEL_MOUSE_LBPRESSED|PANEL_MOUSE_ONREPLAY,
panel2dtex, pel[0]);
RegisterPanelArea (hPanel, AID_MFD_RBUTTONS, _R(x2,y0,x3,y1),
PANEL_REDRAW_USER,
PANEL_MOUSE_LBDOWN|PANEL_MOUSE_LBPRESSED|PANEL_MOUSE_ONREPLAY,
panel2dtex, pel[1]);
...
}
We also need to implement the clbkPanelMouseEvent and clbkPanelRedrawEvent callback functions, but they have a simple form thanks to the common interface of the PanelElement class:
Code:
bool MyVessel::clbkPanelMouseEvent (int id, int event, int mx, int my,
void *context)
{
if (context) {
PanelElement *pe = (PanelElement*)context;
return pe->ProcessMouse2D (event, mx, my);
} else
return false;
}
bool MyVessel::clbkPanelRedrawEvent (int id, int event, SURFHANDLE surf,
void *context)
{
if (context) {
PanelElement *pe = (PanelElement*)context;
return pe->Redraw2D (surf);
} else
return false;
}
Code:
void MyVessel::clbkMFDMode (int mfd, int mode)
{
switch (mfd) {
case MFD_LEFT:
oapiTriggerRedrawArea (0, 0, AID_MFD_LBUTTONS);
oapiTriggerRedrawArea (0, 0, AID_MFD_RBUTTONS);
break;
}
}
Compile your code and try out your panel. If everything works, you can implement the right MFD in the same way.
Part 3 of the tutorial adds a switch ...
(*) Note that Instrument.h and Instrument.cpp are not yet located in Samples\Common\Vessel as of Orbiter100830 (they will be moved there in the next beta). For now, you can find them in Samples\DeltaGlider.