There was a request for a Time Monitor app that would show splits and the lap time including delta’s. Currently LFS, shows you your splits and laps, but only briefly and without any information about how it compares to the previous lap. But with the LFS button system, custom HUD elements can now be created relatively simply. Really, “Button” is a misnomer. It’s really a label control that can be set to clickable.
I figured the time monitor would be a good example to use the RaceTracker class I wrote a couple of days ago and exercise the button system to display the data gathered by the tracker. The resulting app, is a relatively simple winforms sample (the UI exists purely for configuration) that produces LFS stats like this (click image for full screenshot):
The UI for the app itself just lets the user specify how to connect to LFS

The app is not meant for LFS servers, but as a local Aid for clients. It connects to a client instance and displays the buttons locally, according to the currently viewed player Id. I.e. it switches the hud as you tab through the players. It won’t start displaying anything until it hits a first split (i.e. if you just connected and the viewed player has already past the first split, no statistics for the player will show up until the next split 1 pass.
The code is a Visual Studio Solution, created in VS 2005, although it should work fine in Visual Studio Express. It can be downloaded from here. It’s written in C# and uses the LFSLib.NET library for InSim connectivity. It’s not meant to be an App, and you won’t get any support from me for the binary. You can run it as is, but it’s meant to show how LFSLib.NET can be used for tracking and creating buttons. As such, this is example code and completely free of any license (LFSLib.NET is still governed by the GPL, though). Use it however you wish.
I had a request for an example of Win Forms with OutGauge. This is a very simple project that illustrate how to quickly wire up output from OutGauge to UI. The only tricky bit for novices is the whole Invoke business. I.e. updates from outgauge come from it’s listener thread. So you cannot use the event handler for outgauge to directly update your UI, or you’ll get Cross-Thread exceptions. The solution is the common Invoke Required boilerplate:
private void outGaugeHandler_Updated(object sender, Gauge gauge) { // this code is required for Windows Forms, since our OutGauge update // comes from another thread and we can only perform form updates on // the UI thread if (this.InvokeRequired) { BeginInvoke(new OutGaugeHandler.GaugeEvent(outGaugeHandler_Updated), sender, gauge); return; } // now we update the form with the new data // text boxes timeTextBox.Text = gauge.Time.ToString(); carTextBox.Text = gauge.Car; [...] }
The resulting App, has a single window that refreshes OutGauge updates at the interval specified in the Live For Speed cfg.txt.
The Solution for this project is for Visual Studio 2005, but should work just fine in Visual C# Express. The zip of the project can be found here
Update: LFSLib.NET 0.14b has a bug where an OutGauge ID 0 zero would cause an exception. You can avoid this by using an OutGauge ID higher than 0 or wait for 0.15b to be released. Same thing is true for OutSim.
Continuing on my recent asynchronous UI adventures, my background object tried to access the SelectedItem of a ComboBox and I came crashing down with the dreaded Cross-Thread exception. At this point most of my code has the if(InvokeRequired) boilerplate down….
And every time i cut-and-paste that snippet and tweak if for the current method, I again wonder why the UI framework can’t do some Auto-Invoking magic. But I digress.
…The scenario this time was one of data access to the plain, call BeginInvoke and return approach didn’t apply. This was a scenario I hadn’t yet seen. Unlike normal delegate BeginInvoke, you don’t provide a callback for the EndInvoke. Control.BeginInvoke works differently, but really, it also works simpler. It’s just that it’s not really pointed out on MSDN how the Begin and End fit together in the scope of a Control. The other issue was that I was accessing a property, so there wasn’t a simple delegate for invoking it. Instead I created a simple wrapper Accessor Method for ComboBoxen:
public delegate object GetSelectedItemDelegate(ComboBox control); public object GetSelectedItem(ComboBox control) { if (this.InvokeRequired) { IAsyncResult ar = BeginInvoke(new GetSelectedItemDelegate(GetSelectedItem), control); return EndInvoke(ar); } return control.SelectedItem; }
When you create a Custom Control or User Control in Visual Studio 2k5, it needs to be able to render its running state with no additional information. I.e. it needs to have a no argument constructor and cannot need extra data passed in at initialization, otherwise Visual Studio 2k5 Designer will die when you add the Control to another form or control, even if it comes up fine in the Designer by itself.
Ok, this one is a big Duh! but i’m sure i’ll forget again some day in the future and bitch and moan about Visual Studio hating me and constantly crashing. Basically it comes down to the designer embedding the control and running it. So OnPaint will get called and any timer you have that’s not disabled will run. Now if you build controls that require extra initialization data, and the running portions of the control try to use them, you’ll get a NullException. Makes perfect sense really, hence the Duh. But of course, you may forget that part, because, after all, the control by itself runs fine and only crashes if the control is added to another control/form. My bet is that the designer is basically a control container and will just discard the exception, but if you add the control to another, the control being added to doesn’t handle the exception and complaints.
A cool little side-effect here is that that you can put an animation in a control and that animaton will run in the Designer.
Came across an annoying bug with ListBox. If you use BeginUpdate() and EndUpdate() and only add a single item to a cleared Item collection, then that Item does not show up on Refresh. However if you leave the BeginUpdate()/EndUpdate() out, it works fine, resulting in a need for code like this:
listBox.Items.Clear(); if( itemList.Count > 1 ) { //only use the Begin/End cycle if we have more than one item itemList.BeginUpdate(); } foreach( object item in itemList ) { listBox.Items.Add(item); } if( group.Strings.Count > 1 ) { //only use the Begin/End cycle if we have more than one item mStrings.EndUpdate(); } listBox.Refresh();
A little System.Windows.Forms diversion. One thing that’s ill-documented, imho, is the CheckedListBox. And if you then want to add a context menu that’s context sensitive to the Item in the list you are over, then good luck finding any useful info. Therefore, I figured i put this snippet in here before I forget again how do deal with this.
The best example I found for handling ContextMenu in the context of a list item recommended completely ignoring the ContextMenu property on the listbox and handling it all in the MouseDown event–creating the context menu on the fly and all.
That seemed a little overkill to me since all i wanted to to do was disable the one item Edit on the context menu. So, instead i capture the Popup event and tweak my menu there. First the setup of the context menu (in my constructor):
ContextMenu contextMenu = new ContextMenu(); MenuItem edit = new MenuItem("Edit"); contextMenu .MenuItems.Add(edit); edit.Click += new EventHandler(edit_Click); contextMenu .Popup +=new EventHandler(contextMenu _Popup); checkedListBox.ContextMenu = contextMenu;
And here’s the popup handler:
private void contextMenu_Popup(object sender, EventArgs e) { //don't forget to translate the mouse coordinates to the control local coordinates int index = checkedListBox.IndexFromPoint(checkedListBox.PointToClient(MousePosition)); if( index == -1 || !checkedListBox.GetItemChecked(index) ) { ((ContextMenu)sender).MenuItems[0].Enabled = false; } else { ((ContextMenu)sender).MenuItems[0].Enabled = true; //storing the index in a class field so that i don't have to //do the math again and in case the mouse moved contextSelection = index; } }
So there you have it.