Wednesday, February 13, 2013

Exploratory Project with Linux C# MonoDevelop Widget Events


Exploratory Project with Linux C# MonoDevelop Widget Events

Background:

It's been a while in coming but the light dawned yesterday and now widget events (e.g., button clicks) are received by the MonoDevelop WinForms solution that was imported from the Microsoft Visual C# Express project.

This is the C# code to implement a PC based ARINC-661 Cockpit Display System (CDS) application without the "Cockpit".  Call it a PCDS maybe - Personal Computer Display System.  Now it runs
A) under Windows via the GNAT Ada user applications that communicate via the A661 protocol with the C# display application and
B) under Linux Mint via the same Ada application (with a Named Pipe interface package replacing the Microsoft MS Pipe interface package and the use of special packages to interface to Linux replacing Win32Ada).  The display application runs via MonoDevelop.  Presumably the MonoDevelop display application could also be used with Windows.

Initially I used Fedora Linux.  The MonoDevelop that I used with Fedora couldn't handle the .NET 4.0 that I was using with C#.  But very little adjustment was necessary to display the WinForm that took the place of an A661 layer.  However, the application failed to receive widget (Microsoft control) events such as button clicks except for the form close (X) button.

I then tried MonoDevelop with a Windows download with the same result.

Next I installed Linux Mint Cinnamon version 14 as mentioned in the previous blog post (Exploratory Project with Linux C# MonoDevelop).  With it I was able to revert back to the WinForm .NET 4.0 code (only a change to how the queue was inherited that allowed the display threads to obtain A661 commands read from the named pipe for the particular layer that ran, of course, in their own receive threads - this to satisfy Windows problems with needing the same thread to paint the display form as to get the events).

Again the widget events weren't received by the MonoDevelop display application. 

Trying to figure out what to do I had done a number of internet searches and had come up with various bits of information.  One saying that widgets had to be wrapped inside a special event container for GTK to receive them.

I had also been playing with GTK and so got sample code running this way.  (See Linux Mint of the previous post.)  However, in attempting that I ran into the wall that WinForms and GTK shall never mix.  So I couldn't put a GTK event container such as VBox around the widgets of the form.

Finally:

Finally I found a "First steps in Mono Winforms" post (zetcode.com/gui/csharpwinforms/firststeps/) that said, yes indeedy, it is possible to get click and other events using a WinForms MonoDevelop application.  I made myself a test solution using the supplied sample code and got the OnEnter and OnClick events that it illustrated.

Now I knew there had to be a solution to my problem other than rework the display application into a Gtk# application.

The Solution:

I first tried various versions of the sample code with the form of the Exploratory Project display app.  What had worked in the standalone sample MonoDevelop solution with its MForm class derived from the Windows Form class just didn't seem to work with my form.

After tearing out a little more of my hair, the light suddenly dawned, the epiphany arrived. 

In the sample the form was just created.  That is,
 class MForm : Form {
    public MForm() {
    . . .
    }

    void OnClick(object sender, EventArgs e) {
       Close();
    }

    void OnEnter(object sender, EventArgs e) {
       Console.WriteLine("Button Entered");
    }
 }

 public static void Main() {
    Application.Run(new MForm());
 }

Whereas in the A661 Exploratory Project case it was created and then set (temporarily) to disabled.

  public class UserForm : Form
  {
    . . .
    public UserForm(int formIndex)
    {
      initializeForm(formIndex);

    } // end UserForm constructor

    private void initializeForm(int formIndex)
    {
      . . .
      if (!Display.widgetsCreated[formIndex])
      {
        widgetIndex = formIndex;
        widgetId = Display.formDesc[formIndex].formId;
        layerId = Display.formDesc[formIndex].layerId;
        layerIndex = Display.formDesc[formIndex].layerIndex;

        buttonCount = 0;
        checkBoxCount = 0;
        listBoxCount = 0;
        labelCount = 0;
        textBoxCount = 0;

        myControl.count = 1;
        myControl.depth = 0;
        myControl.controls = new Control.ControlCollection[6];
        for (int i = 0; i < 6; i++)
        {
          myControl.controls[i] = this.Controls;
        }

        containerCount = 0;
        panelCount = 0;

        Display.widgetsCreated[formIndex] = true;
      }

      this.SuspendLayout();

      // Set widget index and id of form for lookup as another kind of widget.
      widgetIndex = formIndex;
      widgetId = Display.formDesc[formIndex].formId;

      //
      // Form
      //
      string formId, formDesc, formParentId, formBorder;
      float scaleWidth, scaleHeight;
      int sizeWidth, sizeHeight;
      int parentFormIndex = -1;
      FormDisplay.formScale scale;
      FormDisplay.formSize size;
      formId = Display.formDesc[formIndex].formId;
      formDesc = Display.formDesc[formIndex].formDesc;
      formParentId = Display.formDesc[formIndex].parentId;
      formBorder = Display.formDesc[formIndex].formBorder;
      scale = Display.formDesc[formIndex].formDimension;
      size = Display.formDesc[formIndex].formSize;
      scaleWidth = scale.width;
      scaleHeight = scale.height;
      sizeWidth = size.width;
      sizeHeight = size.height;
      int x, y;
      x = Display.formDesc[formIndex].xGet(Display.formDesc[formIndex]);
      y = Display.formDesc[formIndex].yGet(Display.formDesc[formIndex]);
      if (x != 0 || y != 0)
      {
        this.Location = new System.Drawing.Point(x, y);
      }
      else
      {
        this.Location = new System.Drawing.Point(50, 220);
      }
      this.AutoScaleDimensions = new System.Drawing.SizeF(scaleWidth, scaleHeight);
      this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
      this.ClientSize = new System.Drawing.Size(sizeWidth, sizeHeight);
      this.Name = formId;
      this.Text = formDesc;

      if (formBorder == null || formBorder.Equals("NONE"))
      {
        this.FormBorderStyle = FormBorderStyle.None;
      }

      this.Load += new System.EventHandler(this.Form_Load);

      createWidgets(formIndex);

      // determine if top level form or child
      parentFormIndex = Display.widgetLoad.getFormIndexForId(formParentId);
      if (parentFormIndex >= 0)
      {
        this.TopLevel = false; // indicate that a child
        // add child form to parent form
        Display.userForm[parentFormIndex].Controls.Add(this);
      }
      else
      {
        this.TopLevel = true; // indicate parent form
      }
      this.ResumeLayout(false);
      this.PerformLayout();
      this.Enabled = Display.formDesc[formIndex].enabled;

    } // end method initializeForm

Most of the various information used above comes from parsing the xml of the A661 Definition File that specifies what the layer (and hence the Form) is to look like.  At the end of the above code, this.Enabled was set to false since Enabled was False in the Definition File - that is,
<A661_LAYER
      DESCRIPTION="GUI1 Layer"
      LAYER_IDENT="3002"
      NET_PORT="/home/clayton/Source/EP/DisplayLayer"
      POS_X="0"
      POS_Y="0"
      SIZE_X="800"
      SIZE_Y="800"
      UNIT_NAME="GUI1"
      FORM_IDENT="3000"
      ENABLE="A661_FALSE"
      VISIBLE="A661_TRUE"
      SCALE_WIDTH="6"
      SCALE_HEIGHT="13"
      FORM_BORDER="FixedDialog">
so the form, as initialized in the call from the constructor, is initially disabled.  (Note: This xml is used to create the form that takes the place of the layer in the PC implementation.)

The form becomes enabled when the display app (the PC version of the A661 CDS) receives the A661 Enable Layer command. 

I changed the statement at the end of initializeForm to
     this.Enabled = true;
to have the form enabled from the start. 

With this change, the button clicks were received and hence were forwarded as Event Messages to the user app (app1) via the named pipe.  The user app then routed them to the Widget Action (WA) component associated with the widget identifier as illustrated in the diagram of the last week's post.  The Widget Action component then created the command to modify the display and it was transmitted back to the display app and the display was modified.

Therefore, MonoDevelop is only allowing widget events from a form (except for the form close widget in the upper right corner) when the form is enabled when it is created and isn't recognizing a later enable of the form.

Consequences:

I had set ENABLED in the xml to FALSE for the form so that all the widgets of the layer (that is, form) would be disabled until the user application sent the command to enable the layer.  (Note: Under A661, to be enabled, a child widget of a container must be enabled and its parent container must be enabled all the way back to the top level container which in this case is the form.  Therefore, with the form disabled, all the widgets were disabled.)

Needing to have the form enabled from the beginning for the widget events to be received meant I had to find a different way to have the upper level container disabled until the layer is commanded to be enabled.

Volia!  I just happened to have the solution designed in from the start.  That is, my Definition File xml has just such a container as follows.

<A661_LAYER
      DESCRIPTION="GUI1 Layer"
      LAYER_IDENT="3002"
      NET_PORT="/home/clayton/Source/EP/DisplayLayer"
      POS_X="0"
      POS_Y="0"
      SIZE_X="800"
      SIZE_Y="800"
      UNIT_NAME="GUI1"
      FORM_IDENT="3000"
      ENABLE="A661_FALSE"
      VISIBLE="A661_TRUE"
      SCALE_WIDTH="6"
      SCALE_HEIGHT="13"
      FORM_BORDER="FixedDialog">
      <WIDGET
            DESCRIPTION="GUI1 Layer"
            WIDGET_TYPE="A661_BASIC_CONTAINER"
            WIDGET_IDENT="3101"
            PARENT_IDENT="3000"
            ENABLE="A661_FALSE"
            VISIBLE="A661_TRUE"
            POS_X="0"
            POS_Y="0">

That is, I have had a container as the direct child of the form that represents the layer.  This container has widget 3000 as its parent which is the widget that has that identifier.  No other widget of the xml references 3000 as its parent.  The direct children all reference the layer container widget 3101.  Just what is needed. 

So all it took was to change the ENABLE of this container widget to FALSE in the Definition File as shown above.  This made all the child widgets disabled (grayed out) when the form is first displayed.  The receive of the Make Layer Enabled command was already implemented to make this widget enabled along with the form.

So the solution to the lack of receiving widget events was quite simple once I figured out the reason that the events weren't being received.  Just, oh the hide and seek game in the meantime.

Now the xml can be changed once more to set ENABLE of widget 3000 (the form) to TRUE and restore the initializeForm statement back to using the value from the Definition File.

This was done and all is happy.  The MonoDevelop now sets the initial form enable as before but the xml has that enable should true.  What wouldn't work before now works with only these two changes to the Definition File.


No comments: