Developer masterclass: Creating 2-D panels (Part 2)

martins

Orbiter Founder
Orbiter Founder
Joined
Mar 31, 2008
Messages
2,448
Reaction score
462
Points
83
Website
orbit.medphys.ucl.ac.uk
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:
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);
}
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:
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;
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:
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
};
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:
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;
}
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:
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;
}
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.
Code:
class MyVessel: public VESSEL3 {
  ...
  PanelElement *pel[2];
  ...
};
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:
Code:
MyVessel::MyVessel (...)
{
  ...
  for (int i = 0; i < 2; i++)
    pel[i] = new MFDButtonCol (this, i);
  ...
}
and delete them in the destructor:
Code:
MyVessel::~MyVessel ()
{
  ...
  for (int i = 0; i < 2; i++)
    delete pel[i];
  ...
}
Inside the DefineMainPanel method, we register the button columns as active regions:
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]);
  ...
}
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:
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;
}
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.
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;
  }
}
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.
 

Bibi Uncle

50% Orbinaut, 50% Developer
Addon Developer
Joined
Aug 12, 2010
Messages
192
Reaction score
0
Points
0
Location
Québec, QC
Thank you Martin ! A wonderful tutorial !
:tiphat:

However, there is a little typing mistake in the fourth code example. You define 2 times your constant variable btnw. The second one should be btnh I think.

EDIT:
Oh ! I found another mistake. I was stuck with it. In the sixth code sample, the PanelElement pel[2] must be a pointer to an array, like this : PanelElement *pel[2]. I checked in the DG sample to find the solution. Also, to have a result on screen, change the draw event from PANEL_REDRAW_USER to PANEL_REDRAW_ALWAYS. However, to save ressources, let the draw event to PANEL_REDRAW_USER and refresh it only when in clbkMFDMode.

EDIT 2:
:woohoo:
It finaly works ! I was scrambled between texture coordinates and panel coordinates... A little screen for fun (very basic panel, I know :lol: It's my first 2D panel with the new interface )
MFD.jpg
 
Last edited:

martins

Orbiter Founder
Orbiter Founder
Joined
Mar 31, 2008
Messages
2,448
Reaction score
462
Points
83
Website
orbit.medphys.ucl.ac.uk
I know it's been a while, but I just revisited the tutorial, and finally updated it with Bibi Uncle's corrections. Thanks for pointing out the mistakes!
 
Top