« Scrabble Playability in Hoot | Programmatically - Let Designer show you » |
Hoot With a Ribbon, MDI, and Inheritance
I just released a new beta version of Hoot. While it is fully functioning, it does involve significant changes in the way Hoot works. With version 1.9.0, Hoot employs an MDI (Multiple document interface). Previously, Hoot opened new windows for different types of searches. These windows filled the taskbar and were scattered across the desktop. With MDI all windows are contained in the program's main menu.
Another significant difference is the use of inherited forms. While users should not care, it does mean the code is smaller and (theoretically) faster. For me, it means changes can be made to the interface easier.
Finally, the new Hoot includes a ribbon, like you might have seen in Microsoft Office applications beginning in 2007. While this ribbon is not from Microsoft, it is a very good look-alike. All of this may sound boring, but for a developer it is a little fascinating, figuring out how things work, as well as how to work around limitations.
Multiple Document Interface
With the MDI I could avoid creating new form for each call and instead open them as child windows. Other than setting the appropriate flags on the parent form, the only thing I had to do different is open the forms appropriately for the MDI. One standard way to open such forms is with code like what I've used. This also cascades the new form over the last active child.
private void OpenChild(Form child)
{
child.MdiParent = this;
Form last = new Form();
if (this.MdiChildren.Count() > 0)
foreach (Form f in this.MdiChildren)
if (f == this.ActiveMdiChild)
{
last = f;
child.Location = new Point(last.Location.X + 25, last.Location.Y + 25);
break;
}
child.Show();
}
If I want to insure only one copy of that form is used I simply check for the form name among the children.
private void OpenOneChild(Form child)
{
if (!(FindChild(child.Name) == null))
{
child.Activate();
return;
}
OpenChild(child);
}private Form FindChild(string name)
{
foreach (Form f in this.MdiChildren)
{
if ((string)f.Text == name)
return (f);
}
return null;
}
Of course, if I wanted to open a form from a child window, I would use a different method.
private void OpenSibling(Form child)
{
child.MdiParent = this.ParentForm;
Form last = new Form();
if (this.ParentForm.MdiChildren.Count() > 0)
foreach (Form f in this.ParentForm.MdiChildren)
if (f == this.ParentForm.ActiveMdiChild)
{
last = f;
child.Location = new Point(last.Location.X + 25, last.Location.Y + 25);
break;
}
child.Show();
}
Inheritance
When I mentioned Inherited forms on one discussion board one of the responses started with "ouch, ouch, ouch". Well, it's not so bad. With inheritance in C# you can't normally pass parameters to an inherited form, so I found a workaround. While inherited forms apparently can't take parameters in their constructors, at least not directly, you can access the controls of the inherited form and force a parameter.
1. Add a label named lblTrigger (public or protected)
2. Add an event to the label for the TextChanged event
private void lblTrigger_TextChanged(object sender, EventArgs e)
{
switch (lblTrigger.Text)
{
case "Hooks": ShowHooks(); break;
case "Words": HideHooks(); break;
case "Slides": btnSlideShow_Click(sender, e); break;
case "ChangeLanguage": PopulateResources(); break;
}
lblTrigger.Text = "Trigger";
}
3. Change that label from the ribbon or another form
public static void SetTrigger(string text, System.Windows.Forms.Form frm) {
if (frm == null)
return;
foreach (System.Windows.Forms.Control ctl in frm.Controls)
{
if (ctl.Name.Equals("lblTrigger"))
ctl.Text = text;
}
}
You're not limited to one label, either. You can add parameters to use with a different label. That's what I've done using lblParameters included in one of the code segments below. Set the parameters, then "pull" the trigger.
In one case "SavedSearch" is the text I set for the trigger.
case "SavedSearch": OpenFromRecent(); break;
That then calls this method which simulates a button click on the form.
private void OpenFromRecent()
{
specs = Utilities.LoadSearch(lblParameters.Text);
if (specs == null)
return;
cboSearchType.SelectedIndex = Array.IndexOf(english, specs.searchtype);
if (!(cboSearchType.SelectedIndex == -1))
{
txtSearch.Text = specs.letters;
cboMin.SelectedIndex = specs.minLen - 1;
cboMax.SelectedIndex = specs.maxLen - 1;
cboBegin.Text = specs.prefix;
cboEnd.Text = specs.suffix;
txtFilter.Text = specs.filter;
}
btnSearch_Click(this, EventArgs.Empty);
}
Ribbon
The ribbon I am using in this project is the one mentioned in the Code Project article at https://www.codeproject.com/articles/364272/easily-add-a-ribbon-into-a-winforms-application-cs. It's a third-party ribbon for C#, not WPF or other Microsoft. While it looks and performs well, there is a limited documentation for it. The intellisense and integration with Windows is good, and many items are similar. One of my biggest challenges was determining how to use the Recent Items list.
Recent items
One thing that was not so easy to understand was the use of the Recent Items list in the Orb menu. Determining how to create recent items dynamically, however, was one of the biggest accomplishments recently. My last blog entry touched on recent items and using the C# .Designer file to figure out how to create them. Actual implementation was a little more difficult. With my implementation I created a new class with properties of the recent item that I would use.
The Recent Item
public RecentItem(string tag, string text)
{
ItemID = 0;
ItemTag = tag;
ItemText = text;
UserID = Properties.Settings.Default.User;
}
The text is the filename. The tag indicates the type of item since the items are not identified by file extension. ItemID will be updated when saved to the database. The database fields are similar to the class fields.
Loading Items on form load:
..
List<RecentItem> load = new List<RecentItem>();
load = DBManager.LoadRecentItems();
AddRecentItems(load); // see below
ribbonMain.OrbDropDown.RecentItems.Reverse();
..
Reverse is used to put latest items at the top. The ID is set by the database using autonumber so the oldest items have the lowest ID and are loaded first.
private void AddRecentItems(List<RecentItem> recentItems) {
foreach (RecentItem item in recentItems)
DBManager.OrbAddRecentItem(item); // see below
}
Adds items to Recent Items from database
public static void OrbAddRecentItem(RecentItem recent)
{
frmHootGold top = (frmHootGold)System.Windows.Forms.Application.OpenForms["frmHootGold"];
RibbonOrbRecentItem adder = new RibbonOrbRecentItem();
adder.Tag = recent.ItemTag;
adder.Text = recent.ItemText;
adder.Value = recent.ItemID.ToString();
top.ribbonMain.OrbDropDown.RecentItems.Add(adder);
adder.Click += new System.EventHandler(top.OrbRecent_Click); // see below
}
When the items are added to the list, either as new items or when loaded from the database, the click event is set.
Calling Form
A separate method is used when actually adding a new item that has one difference to add the new item to the list that has already been sorted in descending order.
public static void OrbInsertRecentItem(RecentItem recent)
..
top.ribbonMain.OrbDropDown.RecentItems.Insert(0, adder);
That's done from one of the child windows with code segment something like this
RecentItem adder = new RecentItem("SavedSearch", searchFile);
DBManager.AddRecentItem(adder);
DBManager.OrbInsertRecentItem(adder);
The Recent Item List
When a user clicks an item on the Recent list, the following method uses the trigger and parameters mentioned earlier to pass that information to a new child form.
public void OrbRecent_Click(object sender, EventArgs e)
{
RibbonOrbRecentItem item = (RibbonOrbRecentItem)sender;
Form form = new Form();
switch (item.Tag.ToString())
{
case "SavedSearch": form = new frmCombo(); break;
case "Textfile": form = new frmTextfiles(); break;
}
OpenChild(form);
MDIUtils.SetParms(item.Text, form);
MDIUtils.SetTrigger((string)item.Tag, form);
}
One of the things I learned about Recent Items is that there is no obvious way to enable user-deletion of items. I notice Word doesn't have that either, nor does Windows, so I decided not to worry about it.
If you use this in a program you may notice that the items are added to Windows list of Recent Items, but it's not because of this method. It's simply because they were files that "you" opened. Anyway, this is my contribution to the C# Ribbon knowledge base.