Q. Sink Notifications From Arrays of Objects
I've written some classes that raise events back to their clients. I'd like to use arrays of these classes, but VB won't allow that together with WithEvents:
Dim WithEvents MyClass() As Class1
I need a strategy for listening to events from many objects. Any ideas?
ABOUT THIS COLUMN
Ask the VB Pro provides you with free advice on programming obstacles, techniques, and ideas. Read more answers from our crack VB pros. You can submit your questions, tips, or ideas on the site, or access a comprehensive database of previously answered questions.
|
|
A.
Because you're in control of all the source, I can give you a strategy that not only allows this, but is also more efficient than the standard event method. In a nutshell, you can provide an interface for your clients to implement, and have the objects notify this interface in place of raising events.
If you've never seen this type of scheme in action, swing by the Common Controls Replacement Project (CCRP ) Web site (see Resources) and grab a copy of the High Performance Timers Library. I wrote this handy DLL with just such a need in mind. ObjArray, one of the demos that comes with the library, shows how to set up an array of Timer objects within a project. I'll guide you through the basic concepts.
First, you need to define the notification interface, which ranges from extremely simple to extraordinarily complex, depending on your needs. Add a new class to your project and name it INotify. Add a Public subroutine to it, with any needed parameters you'd like to raise along with your notification. Also, add a parameter through which you pass a reference to your object:
' Interface code...
Public Sub MyEvent(ByVal X As Long, _
ByVal obj As MyObject)
'
End Sub
Add as many notification procedures as your object requires, but remember that the interface class contains no code other than the procedure definitions themselves. Think of each procedure as corresponding to an event. The last parameter, a reference to the notifying object itself, takes the place of the Index parameter VB normally passes to control array events for identifying which instance is calling. The clienta form, control, or other classnow Implements this interface by exposing all the interface's methods:
' Client code...
Implements INotify
Private obj(0 To 5) As MyObject
Private Sub INotify_MyEvent(ByVal X As Long, _
ByVal obj As MyObject)
'
End Sub
Your objects also need to add a property through which the client identifies itself to each object. In the classes raising the notifications, add a Notify property, which stores a reference to the client:
' Object code...
Private m_Client As INotify
Public Property Set Notify(ByVal Client As _
INotify)
' Hold a reference to client
Set m_Client = Client
End Property
This code sets up a major issue you'll want to considerwhether to maintain a circular reference or simply hold a weak reference. The weak reference method is far cleaner, but also requires more detail than I can explain here. Previous columns of mine contain more background on this topic (see Resources). Storing a strong reference requires your client to clear the reference explicitly, or else neither object terminates.
You'll also want to provide the object with some ready means of identification. With the CCRP Timers, I chose to add the standard Tag property:
' Object code...
Private m_Tag As Variant
Public Property Let Tag(ByVal NewVal As _
Variant)
' Store user-supplied data.
m_Tag = NewVal
End Property
Public Property Get Tag() As Variant
' Return user-supplied data.
Tag = m_Tag
End Property
Now, back to the client. You're ready to create an array of objects and pass them references to the client:
' Client code...
Private Sub Form_Load()
Dim i As Long
For i = 0 To 5
Set obj(i) = New MyObject
Set obj(i).Notify = Me
obj(i).Tag = i
Next i
End Sub
As I mentioned, you also need to clear the stored reference from the client side before shutdown:
' Client code...
Private Sub Form_Unload(Cancel As _
Integer)
Dim i As Long
' Allow normal termination.
For i = 0 To 5
Set obj(i).Notify = Nothing
Set obj(i) = Nothing
Next i
End Sub
Now you're ready to rock! Your objects are free to call the Notify method of the client in place of raising events, or if no notification client has been specified, the objects might fall back to raising an event. The client can identify the calling object through use of some unique property instead of the more standard Index parameter:
' Object code...
Private Sub Whatever()
...
' Something that requires an
' "event"
If Not (m_Client Is Nothing) Then
m_Client.MyEvent X, Me
Else
RaiseEvent MyEvent(X)
End If
...
' Client code...
Private Sub INotify_MyEvent(ByVal X _
As Long, ByVal obj As MyObject))
'
Select Case obj.Tag
Case 0
...
Although it would be nice if With-Events supported arrays of objects, that simply isn't part of the COM model. It's lucky that control arrays preceded COM, because Microsoft had to really hack that Index parameter to maintain backward compatibility, essentially intercepting the event and injecting the added parameter. As complicated as my solution might seem, it's far easier than the problem Microsoft faced.
Q.
Reset DateTimePicker Focus
When a user changes the value in a DTPicker control, then tabs to another control and back to the DTPicker control, the focus is always on the last field the user modified. For example, when the control is in time format, the focus might be on the minutes field when the user tabs away. Upon tabbing back to this control, focus returns to the minutes field, not to the first (hours) field. Can I force the focus always to the left-most field of the DTPicker Value?
A.
I kind of like the default behavior, myself. But I can surely see that in certain situations, such as validation failure, it would be nice to have control over which field has focus. Unfortunately, this control was written without that in mind. No native calls or messages set which input field has focus.
I tried a few really nasty hacks, and wasn't happy with any of them. One that showed some promise was sending consecutive WM_LBUTTONDOWN and WM_LBUTTONUP messages to the control to simulate a mouse click. The problem with this solution is that you're forced to specify coordinates, and if you happen to guess wrong, the wrong fieldor no field at allreceives focus.
In another attempt, I used SendKeys to drop the month calendar, then pull it back quickly:
Private Sub DTPicker1_GotFocus()
SendKeys "{F4}{Esc}"
End Sub
As you might guess, the visual appeal of this solution degrades rapidly on slower machines. It's also entirely useless when you want a time format. Jeremy Adams, a friend and founder of the CCRP, suggested resetting the Format property. And sure enough, this works exactly as you specify:
Private Sub DTPicker1_LostFocus()
Dim fmt As Long
With DTPicker1
Debug.Print Hex(.hWnd)
' store current value
fmt = .Format
' try to use another one
If fmt = dtpLongDate Then
.Format = dtpShortDate
Else
.Format = dtpLongDate
End If
' reset to former value
.Format = fmt
Debug.Print Hex(.hWnd)
End With
End Sub
What's even more interesting is why it works. This property corresponds to styles that must be set at the time the datetimepicker window is created, and cannot be changed afterwards. When the Format property is changed, VB is forced to destroy the existing window and re-create it. You can see this for yourself through the Debug.Print statements in the previous LostFocus routine. Don't force this issue in the GotFocus event because that spawns an endless cascade of events.
By default, when the newly created window gets focus, it highlights the first input field in its display. You can then fall back on SendKeys to move focus to another field, if you like. For example, you can highlight the minutes field in a dtpTime formatted control:
Private Sub DTPicker1_GotFocus()
SendKeys "{right}"
End Sub
Most VB control authors are familiar with the concept of stashing property values, re-creating windows as needed, and restoring the unchanged properties. The datetimepicker control demonstrates Microsoft is also capable of this feat. It's too bad the company hasn't seen its way clear to overcoming the rest of the "read-only at run time" properties.
Q.
Flipping Bits
How can I manipulate each bit of a byte? For example, I have a single byte equal to &HEE (11101110). I want to Xor the bits in positions 7, 4, and 2, and place that value in the most significant bit-in this case, the result being &H6E (01101110).
A.
You have it relatively easy with Byte variables because they're the only unsigned variable type VB provides. To determine a given bit's value, use this formula:
BitValue = ((Byte And (2 ^ Bit)) > 0)
The And operator works by comparing the bits in identical positions and returning a result of 1 when both bits are 1, or 0 if either bit isn't 1. This formula might return a 1 in any given bit position, so comparing the result to 0 tells you whether the specified bit is on or off. Using your numbers, you find that the bits in positions 7, 4, and 2 are 1, 0, and 1, respectively (see Table 1). Xor'ing these together results in 0, which you now need to get into the most significant bit (position 7).
To clear a bit, create an inverse mask by applying the Not operator to 2 raised to the power of the bit position and And'ing this against the original value (see Table 2):
BitClear = (ByteIn And Not 2 ^ Bit)
Similar operations are used to set or toggle any specified bit. But remember, Byte variables are unsigned. If the high-bit indicates sign, as it does with Integer and Long variables, the situation is more interesting. You can no longer use 2^Bit for generating masks on the most significant bit because that positive value overflows the variable type. Instead, if you work with either Integer or Long data, you need to make a special case for the high bit (see Listing 1). Hard-coding these masks is the easiest solution.
One final note: If you find that you're raising 2 to random powers millions of times, as some intense calculations might warrant, it can be much more efficient to precalculate these values and store them in a lookup table. The difference isn't stunning in the IDE or compiled p-code, but processing can be 15 to 20 times faster when you compile the code to native with Integer overflow checking (Advanced Optimizations) turned off.
Karl E. Peterson is a GIS analyst with a regional transportation planning agency and serves as a member of the Visual Basic Programmer's Journal Technical Review and Editorial Advisory Boards. Online, he's a Microsoft MVP and a section leader on several VBPJ forums. Find more of Karl's VB samples at www.mvps.org/vb.