Quantcast
Channel: VBForums - CodeBank - Visual Basic 6 and earlier
Viewing all 1540 articles
Browse latest View live

[VB6] Basic unzip without 3rd party DLL or shell32- IStorage-based

$
0
0
So the only other method I've really seen to extract zip archives without shell32 or a 3rd party DLL is a full implementation of the ZIP algorithm, and while this isn't exactly a lightweight method, it's not nearly as complex as that was with all its class modules. As I've mentioned a few times, I'm definitely not a fan of the shell32 object, and I came across an unzip method using things I do like: shell interfaces. Thanks to low-level Windows ZIP integration, it's possible to extract the contents of a simple ZIP archive (doesn't support password-protected zips for example) using IStorage, IStream, and some API.

Requirements
A type library with IStorage and IStream is required, and I strongly recommend using oleexp for future compability (get it here)- any version is fine, there's no new version like new examples usually need; and the sample project is written for that. However, if you change a couple 'oleexp3.x' declares, the original olelib is supported (for the sample project, you'll need a new way of selecting the zip file too since it's using FileOpenDialog).

This method is compatible with Windows XP and higher, but note the sample project for simplicity has a Vista+ FileOpen

Code
Below is a free-standing module you can use without anything else in the demo project (besides oleexp or olelib with changes):

Code:

Option Explicit

Public Declare Function ILCreateFromPathW Lib "shell32" (ByVal pwszPath As Long) As Long
Public Declare Sub ILFree Lib "shell32" (ByVal pidl As Long)
Public Declare Function SHBindToParent Lib "shell32" (ByVal pidl As Long, riid As UUID, ppv As Any, pidlLast As Long) As Long
Public Declare Function SHCreateStreamOnFileEx Lib "shlwapi" (ByVal pszFile As Long, ByVal grfMode As STGM, ByVal dwAttributes As FILE_ATTRIBUTES, ByVal fCreate As Long, ByVal pstmTemplate As Long, ppstm As oleexp3.IStream) As Long
Public Declare Function PathFileExistsW Lib "shlwapi" (ByVal lpszPath As Long) As Long
Public Declare Function CreateDirectoryW Lib "kernel32" (ByVal lpPathName As Long, ByVal lpSecurityAttributes As Any) As Long
Public Declare Function SysReAllocString Lib "oleaut32.dll" (ByVal pBSTR As Long, Optional ByVal pszStrPtr As Long) As Long
Public Declare Sub CoTaskMemFree Lib "ole32.dll" (ByVal PV As Long) ' Frees memory allocated by the shell
Public Const NOERROR = 0&
Public Const FILE_ATTRIBUTE_NORMAL = &H80

Public Sub UnzipFile(sFile As String, Optional ByVal sTo As String = "")
'unzip without 3rd party dll
Dim psfParent As oleexp3.IShellFolder
Dim pidlFQ As Long
Dim pidlChild As Long
Dim pszDest As String

If sTo = "" Then
    'defaults to create a folder with the zip's name in the same folder as the zip
    pszDest = sFile
    pszDest = Left$(pszDest, Len(pszDest) - 4) 'remove .zip
Else
    pszDest = sTo
End If

'First, we need the parent pidl, child pidl, and IShellFolder
'These are all references to the file very common in shell programming
pidlFQ = ILCreateFromPathW(StrPtr(sFile))
Call SHBindToParent(pidlFQ, IID_IShellFolder, psfParent, pidlChild)
If (psfParent Is Nothing) Or (pidlChild = 0) Then
    Debug.Print "UnzipFile.Failed to bind to file"
    Exit Sub
End If

'Now that we have the IShellFolder, we want the IStorage object
'That is what we'll be able to extract from, thanks to the
'very low level system zip integration with zipfldr.dll
Dim pStg As oleexp3.IStorage
psfParent.BindToObject pidlChild, 0, IID_IStorage, pStg
If (pStg Is Nothing) Then
    Debug.Print "UnzipFile.Failed to bind to storage"
    Exit Sub
End If
Debug.Print "UnzipFile.extract to " & pszDest

StgExtract pStg, pszDest

Set pStg = Nothing
Set psfParent = Nothing
ILFree pidlFQ


End Sub
Private Sub StgExtract(pStg As oleexp3.IStorage, pszTargetDir As String, Optional fOverwrite As Long = 0)
'This function is recursively called to extract zipped files and folders

'First, create the target directory (even if you're extracting to an existing folder, it create subfolders from the zip)
If (PathFileExistsW(StrPtr(pszTargetDir)) = 0) Then
    Call CreateDirectoryW(StrPtr(pszTargetDir), ByVal 0&)
    If (PathFileExistsW(StrPtr(pszTargetDir)) = 0) Then
        Debug.Print "StgExtract.Failed to create directory " & pszTargetDir
        Exit Sub
    End If
End If

'The enumerator will loop through each storage object
'Here, that will be zipped files and folders
Dim pEnum As IEnumSTATSTG
Set pEnum = pStg.EnumElements(0, 0, 0)
If (pEnum Is Nothing) Then
    Debug.Print "StgExtract.pEnum==nothing"
    Exit Sub
End If

Dim celtFetched As Long
Dim stat As STATSTG
Dim pszPath As String

    Do While (pEnum.Next(1, stat, celtFetched) = NOERROR)
        pszPath = SysAllocString(stat.pwcsName) 'contains a file name
'        Debug.Print "pszPath on alloc=" & pszPath
        If (Len(pszPath) > 1) Then
            pszPath = AddBackslash(pszTargetDir) & pszPath 'combine that with the path (recursive, so can be zipped folder path)
'            Debug.Print "pszPath on combine=" & pszPath
            If stat.Type = STGTY_STORAGE Then 'subfolder
                Dim pStgSubfolder As oleexp3.IStorage
                Set pStgSubfolder = pStg.OpenStorage(SysAllocString(stat.pwcsName), 0, STGM_READ, 0, 0)
                If (pStgSubfolder Is Nothing) Then
                    Debug.Print "StgExtract.pstgsubfolder==nothing"
                    Exit Sub
                End If
                StgExtract pStgSubfolder, pszPath 'and if there's more subfolders, we'll go deeper
            ElseIf stat.Type = STGTY_STREAM Then 'file
                'the basic idea here is that we obtain an IStream representing the existing file,
                'and an IStream representing the new extracted file, and copy the contents into the new file
                Dim pStrm As oleexp3.IStream
                Set pStrm = pStg.OpenStream(SysAllocString(stat.pwcsName), 0, STGM_READ, 0)
                Dim pStrmFile As oleexp3.IStream
               
                'here we add an option to not overwrite existing files; but the default is to overwrite
                'set fOverwrite to anything non-zero and the file is skipped
                'If we are extracting it, we call an API to create a new file with an IStream to write to it
                If PathFileExistsW(StrPtr(pszPath)) Then
                    If fOverwrite Then
                        Call SHCreateStreamOnFileEx(StrPtr(pszPath), STGM_CREATE Or STGM_WRITE, FILE_ATTRIBUTE_NORMAL, 1, 0, pStrmFile)
                    End If
                Else
                    Call SHCreateStreamOnFileEx(StrPtr(pszPath), STGM_CREATE Or STGM_WRITE, FILE_ATTRIBUTE_NORMAL, 1, 0, pStrmFile)
                End If
                If (pStrmFile Is Nothing) = False Then
                    'Debug.Print "StgExtract.Got pstrmfile"
                    Dim cbSize As Currency 'the STATSTG cbSize is ULONGLONG (equiv. to Currency), so files >2GB should be fine
                    pStrm.CopyTo pStrmFile, stat.cbSize, 0, cbSize
                    Set pStrmFile = Nothing
                    'Debug.Print "StgExtract.bytes written=" & CStr(cbSize)
                Else
                    'either an error or skipped an existing file; either way we don't exit, we'll move on to the next
                    'Debug.Print "StgExtract.pstrmfile==nothing"
                End If
                Set pStrm = Nothing
            End If
        End If
        pszPath = ""
        Call CoTaskMemFree(stat.pwcsName) 'this memory needs to be freed, otherwise you'll leak memory
    Loop
   
    Set pEnum = Nothing
   

End Sub
Public Function LPWSTRtoStr(lPtr As Long, Optional ByVal fFree As Boolean = True) As String
SysReAllocString VarPtr(LPWSTRtoStr), lPtr
If fFree Then
    Call CoTaskMemFree(lPtr)
End If
End Function
Public Function AddBackslash(s As String) As String

  If Len(s) > 0 Then
      If Right$(s, 1) <> "\" Then
        AddBackslash = s & "\"
      Else
        AddBackslash = s
      End If
  Else
      AddBackslash = "\"
  End If

End Function

Public Function IID_IStorage() As UUID
'({0000000B-0000-0000-C000-000000000046})
Static iid As UUID
 If (iid.Data1 = 0) Then Call DEFINE_OLEGUID(iid, &HB, 0, 0)
 IID_IStorage = iid
End Function

'-----------------------------------------------------------
'Below this is not needed if you're using mIID.bas
'(check if the above IID_IStorage exists or not, because this was released before the update that included it)
'-----------------------------------------------------------
Public Sub DEFINE_UUID(Name As UUID, L As Long, w1 As Integer, w2 As Integer, B0 As Byte, b1 As Byte, b2 As Byte, B3 As Byte, b4 As Byte, b5 As Byte, b6 As Byte, b7 As Byte)
  With Name
    .Data1 = L
    .Data2 = w1
    .Data3 = w2
    .Data4(0) = B0
    .Data4(1) = b1
    .Data4(2) = b2
    .Data4(3) = B3
    .Data4(4) = b4
    .Data4(5) = b5
    .Data4(6) = b6
    .Data4(7) = b7
  End With
End Sub
Public Sub DEFINE_OLEGUID(Name As UUID, L As Long, w1 As Integer, w2 As Integer)
  DEFINE_UUID Name, L, w1, w2, &HC0, 0, 0, 0, 0, 0, 0, &H46
End Sub
Public Function IID_IShellFolder() As UUID
  Static iid As UUID
  If (iid.Data1 = 0) Then Call DEFINE_OLEGUID(iid, &H214E6, 0, 0)
  IID_IShellFolder = iid
End Function

If anyone knows how I could add password support or create a zip file, definitely post ideas in the comments as I'll be working on it.

Thanks
This code is based on code using this method in C by sapero, found here.

------------------
Note: The file I uploaded was named UnzipNew.zip, I have no idea why VBForums keeps renaming it to U.zip. Have tried removing and reattaching several times.
Attached Files

[VB6] Code Snippet: Get/set/del file zone identifier (Run file from internet? source)

$
0
0
So I'm sure everyone has been asked by Explorer whether or not they want to open a file they downloaded from the internet. But how does Explorer know whether or not to ask this question? This information is recorded in an Alternative Data Stream, which is a disk level entry that's attached to the file, but not inside the file itself. Think of it like the date stamps on a file- they're not in the file itself right? A blank text file has them. And not in some database or registry, it's low level associated data on the disk itself.

There's several other uses for alternative data streams, and it's possible to read and write to them like a normal file, e.g. Open "C:\file.txt:Zone.Identifier" For... (if you know their name; not easy but covered by Karl Peterson).

But here we don't have to do that. For the specific case of the Zone Identifier, Windows provides an interface that allows for very simple access to read it, change it, or delete it. IZoneIdentifier, with its default implementation PersistentZoneIdentifier, makes this easy.

Requirements
-Version 3.1 of oleexp, released the same time this post was made, 18 Sep 2015; or a more recent version. Add oleexp3.tlb as a Reference to your project. IDE-only, you don't need to include anything when distributing your compiled app.
-Windows XP SP2 or higher
-This only works on NTFS file systems. If your hard drive is formatted as FAT32 or something else, this does not work.

Code
Code:

Public Function GetFileSecurityZone(sFile As String) As URLZONE
'returns the Zone Identifier of a file, using IZoneIdentifier
'This could also be done by ready the Zone.Identifier alternate
'data stream directly; readfile C:\file.txt:Zone.Identifier

Dim lz As Long
Dim pZI As PersistentZoneIdentifier
Set pZI = New PersistentZoneIdentifier

Dim pIPF As IPersistFile
Set pIPF = pZI

pIPF.Load sFile, STGM_READ
pZI.GetId lz
GetFileSecurityZone = lz

Set pIPF = Nothing
Set pZI = Nothing

End Function

Public Sub SetFileSecurityZone(sFile As String, nZone As URLZONE)
'As suggested in the enum, you technically can set it to custom values
'If you do, they should be between 1000 and 10000.
Dim pZI As PersistentZoneIdentifier
Set pZI = New PersistentZoneIdentifier

pZI.SetId nZone
Dim pIPF As IPersistFile
Set pIPF = pZI
pIPF.Save sFile, 1

Set pIPF = Nothing
Set pZI = Nothing

End Sub

Public Sub RemoveFileSecurityZone(sFile As String)
Dim pZI As PersistentZoneIdentifier
Set pZI = New PersistentZoneIdentifier

pZI.Remove
Dim pIPF As IPersistFile
Set pIPF = pZI
pIPF.Save sFile, 1

Set pIPF = Nothing
Set pZI = Nothing
End Sub

It's that simple. No other code needed.

Now you can go get rid of those prompts for your downloads, or find files that were downloaded.

Thanks
Credit all goes to Raymond Chen at Old New Thing for demonstrating this technique.

VB6 ViewPort-Handling per cairo

$
0
0
A simple Demo, how to work with a larger Image in conjunction with a smaller ViewPort, using
Cairo-ImageSurfaces and their Transformation-methods (Translate, Scale, Rotate),
to make this task a quite easy one...

The code of that Demo is sitting in one single VB-Form only, since it is not very large.

The left Picturebox will support "shifting the original Image per Mouse-Dragging" -
and the two ComboBoxes above it, allow for Rotation and Zooming of that Image.

The semitransparent Selection-Box is adjustable at its four sides per Mouse-Dragging as well,
and the area which this Selection-Box currently covers, is then rendered in the PicBox to the right
(respecting the current Aspect-Ratio of the Selection).

The Main-Window is adjusting the two PicBoxes (picSrc and picDst) dynamically, when it is resized.

The Demo requires the cairo-wrapper-classes from vbRichClient5 (just google for it)...
Here's the SourceCode (containing a TestImage): ViewPortHandlingCairo.zip

And here a ScreenShot:




Olaf
Attached Files

[VB6] Enumerate Services

IStream Manager: Consolidate, Access Stream Data

$
0
0
Attached is a self-contained class that manages IStream objects. These IStreams can be created by the class and/or passed to the class for management and accessibility. The class creates an IStorage interface object and maintains owned streams within the IStorage object. This object can contain up to 2GB worth of stream data that is cached in virtual memory as needed. This allows heavy data to be cached outside the user's memory space, greatly reducing the potential of out-of-memory errors for projects that want to cache large data in memory vs. individual files.

Note: This class is not a full implementation of IStorage. It does not permanently save anything. It was designed as a run-time only virtual cache of data, destroyed on release.

The class organizes IStreams into 2 categories: Owned and External.
Each class method that allows creation of a stream has an option to create it as owned or external.
Owned Streams
Streams that are part of the IStorage object. Owned streams should not be detached from the class. Once the class terminates, any owned streams are invalid
External Streams
These are streams that are created in the class but opted to be created in the user's memory space on an hGlobal memory address. Additionally, any externally created stream that is passed to the class is considered external. External streams can always be detached from the class.

The next post in this thread briefly describes each of the class methods/properties. The class is heavily commented.

This class is not very low level, but it does require some care if you are using a few of its more generic methods which allow you to pass/receive data via memory addresses. You must ensure you do not pass parameters that would cause the class to attempt to read or write past the memory space represented by the passed memory pointer. Crashes will occur. Bottom line. If you tell the class that the available memory exists for the pointer you provided, at least that amount of memory better exist.

Why would you use this? Briefly touched in first paragraph above. Consider a graphics program that allows various levels of 'undos'. Instead of keeping potentially large data in the user's memory space, you can store that data to IStreams and recall them on demand, as needed. If you need to back up source data while you are working with it, while making sure it doesn't get deleted by the user/system, store it an IStream and purge it or recall it as needed. Though 2GB is fairly large, it isn't never ending. This class may make it easy to abuse virtual memory, not the intent.

Couple of super-simple examples. Each IStream is provided a key, the key allows you to choose which IStream you want to access. In the examples below, it is assumed the class is declared at module/form level, public to your project. We'll say the an instance of the IStreamManager class is named: cStrmMgr. In each example, we are assuming the stream's seek pointer is at beginning of the stream. To be sure, we can always call the cStrmMgr.SetSeekPointer method

Example: Back up a 2D array and recall it
Code:

' a Long array was sized as: myArray(0 to 49, 0 to 2499)  and you want to save it
cStrmMgr.AddStreamFromPointer myArray(0,0), 500000, "Spreadsheet1"

' now lets say you want to recall that data
Dim bytesRead As Long
ReDim myArray(0 to 49, 0 to 2499)
' next line identifies the receiving buffer, bytes to read, variable to return bytes read, where to start reading & what Key
cStrmMgr.ReadStream myArray(0, 0), 500000, bytesRead, 0, "SpreadSheet1"
If bytesRead = 500000 Then ' read all data

Example. Read a file into a backup stream
Code:

cStrmMgr.AddStreamFromFile "C:\Temp\SomeFile.any", "History"

' Let's say you don't need the history file any longer and just want to purge it
cStrmMgr.ReleaseStream "History"

Example: Backup a RTF document from the RichtTextBox control
Code:

cStrmMgr.AddStreamFromStringVB RTB.TextRTF, "RTFbackup"

' and lets say you want to return that stream into a VB string:
Dim sText As String
cStrmMgr.SaveStreamToStringVB sText, "RTFbackup"

Example: Detach IStream from the class
Code:

Dim myStream As IUnknown ' or IStream if you have such a TLB
Set myStream = cStrmMgr.StreamObject(myKey) ' get instance of stream
cStrmMgr.ReleaseStream myKey ' detach from class, no longer maintained by the class

Last example. Let's say you are managing undo stuff via a DIB you have a memory pointer for the bits: pDIBits
Code:

cStrmMgr.AddStreamFromPointer pDIBits, picScanWidth * picHeight, "Undo1"

' And now let's say you want to apply that Undo back to the DIB directly
DIm bytesRead As Long, amoutToRead As Long
amountToRead = picScanWidth * picHeight
cStrmMgr.ReadStream pDIBits, amountToRead, bytesRead, 0, "Undo1"
If bytesRead = amountToRead Then ' read all data

Known Limitations
1) 2 GB IStorage object. Trying to exceed this should result in failure
2) 2 GB IStream is largest that can be created and if done, likely to max out IStorage
3) Class does not expose hGlobal address if stream has one. Use GetHGlobalFromStream API
4) Undefined Streams can be added, i.e., length of zero on memory pointer of zero. These can be added to willy-nilly and the stream auto-expands as needed. The 2GB limitation still applies

After downloading the text file, simply remove the .txt extension
Attached Files

[VB6] Push to Pushbullet Demo

$
0
0
Pushbullet is a service and set of applications that can mirror notifications, files, links, SMS messages, etc. among your devices (Android, iOS, PC, browser) and recently added chat features.

The service also offers the PushBullet API, whcih makes it possible for you to write programs that interoperate with the service.

This thread presents a demo program that makes some basic use of the API in VB6.


Requirements

You need a Pushbullet account.

The program requires GDI+ and MSXML 3.0, and should run on any 32-bit x86 (or WOW64) OS from Windows 95 forward as long as these two items have been installed.

Edanmo's IStream interfaces & functions typelib (STRM.tlb, included in attachment).


Scenario

Here we have small (and silly) application: KwayZKalk, a simple four-function calculator.

When an exception is raised (division by zero) KwayZKalk will pop up a Submit Error Report dialog. There the user can enter comments, a contact email address, and then click on Send Report or they can click on Don't Send instead.

If they click Send Report a capture of the main Form is done and pushed to the author along with a summary of the error and the user's optional comments and email address.

The author gets these "pushes" and can use them to take action (normally this would be problem diagnosis) and perhaps contact the user for more information or to send a fix. The author could have another VB6 program to extract these reports and log them into a database for action, but in simple cases they'll just get them on their PC, phone, tablet, wherever they have the Pushbullet app installed.

The program could also be designed to push only to one specific device.


Preparing KwayZKalk

First you need a Pushbullet account. Then you should install one of the client apps or at least go to the site and log on via the browser. This gives you somewhere to receive the "pushes."

Next you can go to your account settings on the web site to retrieve your account's AuthToken value.

Then you can go into the ConfigScrambler subfolder of the attachment and compile ConfigScrambler.vbp, an accessory utility used to apply some trivial encryption. Real applications should use much stronger standards-based encryption techniques than this program does.

Now you can open the template PBConfig.txt file (Unicode JSON text) using Notepad and replace the dummy AuthToken value with your real AuthToken. Save the file and exit Notepad.

Run ConfigScrambler.exe, which will read your PBConfig.txt and write a new PBConfig.dat file. Hex dumps of both are presented by ConfigScrambler.exe to help satisfy your curiosity. Now you can exit the program.

Cut and paste PBConfig.dat from this folder into the parent (KwayZKalk) folder where KwayZKalk can find it.

Finally compile KwayZKalk.vbp to create the dummy application, or you can run it from within the IDE.


Running KwayZKalk

Run the program. It should already be set to divide by zero, so go ahead and click on the "equals" button (just has a horizontal line on it).

This should divide by zero, firing off the Send Error Report dialog. Fill in the comments and email fields and then click on Send Report.

This should screen-capture the KwayZKalk main form and push the image and text information to the Pushbullet service, showing a simple upload animation until complete.

That's about it, and then you should be able to go into the Pushbullet client app on any device (or the web client in your browser) to see the Error Report.


Name:  Flow.png
Views: 94
Size:  15.9 KB

Flow of Error Reporting


Name:  PushCapture.png
Views: 73
Size:  30.1 KB

"Push" as it appears in the Windows PC Client


Beyond KwayZKalk

It is also possible to have your code bundle things up into a ZIP archive and upload and "push" that.

There are also quite a few other things you can do using the Pushbullet API.
Attached Images
  
Attached Files

Easy image disp/edit; scale/rotate, show animated gifs, conv2JPG, +more; No GDI+/DLL

$
0
0
Shell Image Interfaces

Project Description
Windows provides a good bit of image functionality built in and accessible through simple interfaces rather than fairly complicated API. A number of these were added to the latest release of oleexp. This projects demonstrates several of these interfaces:

IShellImageData
Easy to create with ShellImageDataFactory object.
Set pFact = New ShellImageDataFactory
pFact.CreateImageFromFile StrPtr(sISID), pShImg
pShImg.Decode SHIMGDEC_DEFAULT, 10, 10

This is the most useful interface, it can:
-Display basic info about an image (see picture)
-Step frame-by-frame through an animated GIF, or use a timer to 'play' it - I added this feature in after I took the screen shot-- it's included in the attached project
-View multi-page images
-Scale images with different algorithm options e.g. bicubic
-Rotate an image at any angle
-Draw onto a picturebox with transparency
-Save changed image (supports user-defined encoder parameters, but not shown in demo)
...all through single-line calls,
Code:

pShImg.ScaleImage CLng(Text5.Text), CLng(Text6.Text), InterpolationModeBicubic
pShImg.NextFrame
pShImg.Rotate CLng(Text4.Text)
'saving is equally easy:
Dim ipf As IPersistFile
Set ipf = pShImg
ipf.Save sFullPath, 1

IImageTranscode
This interface allows you to convert any image file supported by Windows into a JPG or BMP with only a few lines of code:
Code:

Private Sub DoTranscode(psiSrc As IShellItem, psiDest As IShellItem, nTo As TI_FLAGS)
'The included module provides a standalone implemention of this routine if you're starting
'from only the file paths. This version uses a number of shortcuts getting an IShellItem
'directly from FileOpenDialog gives us

Dim lpDest As Long
Dim pStrm As IStream
Dim pTI As ImageTranscode
Dim pwi As Long, phg As Long

Set pTI = New ImageTranscode

psiDest.GetDisplayName SIGDN_FILESYSPATH, lpDest

Call SHCreateStreamOnFileEx(lpDest, STGM_CREATE Or STGM_READWRITE, FILE_ATTRIBUTE_NORMAL, 1, 0, pStrm)
pTI.TranscodeImage psiSrc, 0, 0, nTo, pStrm, pwi, phg
pStrm.Commit STGC_DEFAULT

Set pStrm = Nothing
Set pTI = Nothing
Call CoTaskMemFree(lpDest)

End Sub

IImageList/IImageList2
These interfaces are very similar to API imagelists (and indeed you can get an API imagelist handle you can use with those functions or assign to a control just by using ObjPtr(pIML)), but apart from being slightly easier to work with also allow resizing on the fly, instead of having to reconstruct. This is also the only way to scale up, because as with API imagelists, you cannot add images smaller than the size the imagelist was created as.
It's important to note than you can create one from scratch, but not with = New ImageList, you need to use ImageList_CoCreateInstance, as shown in the sample project.

Project Requirements
-Windows Vista or higher
-oleexp3.tlb version 3.1 or higher (released 18Sep2015). Only required for the IDE, you don't need to include it with the compiled program.

-----
Some sample images to play around with are included in the ZIP; I didn't make them.
Attached Files

VB6 in AppServer-scenarios (DCOM Replacement per RC5)

$
0
0
In the early days of VB6-usage there was DCOM (later superseded by COM+).

It came with the promise of easy cross-machine-calls (RPCs) by simply using the second
(optional) Parameter [ServerName] of the CreateObject-call...

Now, is there anybody out there (aside from myself), who ever used that (or anybody who's still using it)?
I guess not - and there's a reason for it.

Don't get me wrong - DCOM/COM+ is a great technology - which still works to this day -
*but* - for proper usage you will have to study a few books about that topic, before you
make your first serious steps ... -> right into "config-hell".

So, basically "nice stuff" (and used to this day in some LAN-scenarios, after a "config-orgy"
and countless Proxy-installs on the clients) - but firing it up as easily as the CreateObject-call
suggests? ... Forget about it.

Well, the RichClient5 offers an alternative to DCOM/COM+, which in contrast supports:
- not touching the Registry (serverside Dlls don't need to be registered)
- avoidance of clientside Proxy-installs (to match the interfaces of the serverside COM-Dlls)
- easy movement of the RC5-RPC serverside part to a different Machine per X-Copy of the Server-RootFolder
- same performance as DCOM/COM+ (thousands of Remote-Requests per second in multiple WorkerThreads)
. but using only a single Port ... whereas DCOM/COM+ needs a complete Port-Range
- usable therefore also in Internet-Scenarios, also due to strong authentication/encryption and built-in compression

Ok, so where's the beef - how to use that thing?

Here's the Code for a SimpleRPC-Demo SimpleRPC.zip ...
and a short description with some background follows below...

A finished solution consists of three things (three VB6-Projects):


VB-Project #1: The Server-Application (providing the HostProcess for the AppServer-Listener)
- in the above Zip, this is the Project sitting in Path: ..\RPCServer\RPCServer.vbp

This is the most easy of the three parts, since it is not "ClientApp- or Server-Dll specific" -
just a hosting Exe-Project for the Service which will work with any ServerDll and any Client.

You will only have to compile it once - and can then forget about it...

Here's the complete SourceCode for this ServerHost-Executable (all in a little Form):
Code:

Private RPCListener As cRPCListener 'define the RPC-Server-Listener
Private IP As String, Port As Long, DllPath As String 'Start-Parameters

Private Sub Form_Load()
  'normally this part is contained in a Windows-Service-Executable (without any UI)
 
  IP = New_c.TCPServer.GetIP("")      'get the default-IP of the current machine
  Port = 22222                        'set a Port (22222 is the RC5-RPC default-port)
  DllPath = App.Path & "\RPCDlls\"  'Path, where the Server is looking for the RPCDlls
 
  Set RPCListener = New_c.RPCListener 'create the RPC-Listener-instance
 
  If RPCListener.StartServer(IP, Port, , , , , DllPath) Then '... now we try to start the RPC-Server
    Caption = "Server is listening on: " & IP & ":" & Port
  Else
    Caption = "Server-Start was not successful"
  End If
End Sub

Private Sub Form_Terminate()
  If Forms.Count = 0 Then New_c.CleanupRichClientDll
End Sub

That's it with regards to the ServerHost-instance (a normal UserMode-Executable in our Demo-case).


VB-Project(s) #2: One (or more) ActiveX-Server-Dll(s)
- in the above Zip, this is the Project sitting in Path: ..\RPCServer\RPCDlls\SimpleServerLib.vbp

When you look at the above code for the Service-Host - and its RPCListener.StartServer-function, you will see that it receives a
StartParameter 'DllPath' which in this case points to a SubFolder of the Serverhost-Executable: App.Path & "\RPCDlls\"

And this place (this RPCDlls-Folder) is, where you will have to put your compiled Server-Dlls into.
The Public Subs and Functions you will put into the Class(es) of these Dlls will be, what you later on call remotely
(without the need to register these Dlls).

Here's the whole code of the single Class (cServerClass), this Dll-Project contains -
and yes, you can write this code as any other VB6-Code, as normal Public Subs and Functions
(this little Dll-Project doesn't even have a reference to vbRichClient5, the only reference it contains,
is the one to "ADO 2.5", since it will transfer an ADO-Recordset back to the clientside later on).

Code:

Private Cnn As ADODB.Connection
 
Public Function StringReflection(S As String) As String
  StringReflection = StrReverse(S)
End Function

Public Function AddTwoLongs(ByVal L1 As Long, ByVal L2 As Long) As Long
  AddTwoLongs = L1 + L2
End Function

Public Function GetADORs(SQL As String) As ADODB.Recordset
  If Cnn Is Nothing Then OpenCnn
  Set GetADORs = New ADODB.Recordset
      GetADORs.Open SQL, Cnn, adOpenStatic, adLockBatchOptimistic 'return the ADO-Rs (its content will be auto-serialized)
End Function

Private Sub OpenCnn()
  Set Cnn = New Connection
      Cnn.CursorLocation = adUseClient
      Cnn.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & App.Path & "\Test.mdb"
End Sub

That's it - nothing more is needed for the "active part" of the serverside (the Server-Dlls).
The serverside code is hereby (with #1 and #2) completely finished!


VB-Project #3: The Client-App
- in the above Zip, this is the Project sitting in Path: ..\ClientApp\SimpleRPC.vbp

What remains now, is the clientside part of the RPC - the one which *initiates* an
RPC-(Remote-Procedure-call).

The behaviour (to make the program-flow easier) is in case of the RC5-RPCs *always*
synchronously. That means, that RPCs will not return, until we got a Result, or an
Error-message - or a TimeOut-Error back from such a Remote-Method-call against the Server.

Although also the Clientside-Code is not more than 50 lines or so, I will put only
this smaller excerpt of the client-sides Form-code here into a code-section to explain...:

Code:

Private Const ServerDll$ = "SimpleServerLib.dll" 'Name of the used Dll in the \RPCDlls\-Folder
Private Const ServerCls$ = "cServerClass" 'Name of the Class, which is contained in above Dll
 
Private RPCConn As cRPCConnection 'define the Var for the clientside RPC-connection
 
Private Sub Form_Load()
  Set RPCConn = New_c.RPCConnection 'create the clientside-RPCConnection-instance
      RPCConn.DebugMode = (chkDEBUGMode.Value = vbChecked) 'Debug-Mode (should be switched Off when running as an Executable)
      RPCConn.Host = ""        'put an explicit Server-IP here later on, e.g. read from an Ini-File
      RPCConn.Port = 22222    'Port-Nr the Server is listening on (22222 is the RC5-RPC-default)
      RPCConn.KeepAlive = True 'set KeepAlive for better performance
End Sub

'... snipped the two other Methods, which we also wrap in this Form

Private Sub cmdAddTwoLongs_Click() 'an example Remote-Method-Call
On Error GoTo ErrMsg
 
  txtAdd.Text = RPCConn.RPC(ServerDll, ServerCls, "AddTwoLongs", 3, _
                CLng(txtL1.Text), CLng(txtL2.Text)) '<- Parameter-List (two Long-Values in this case)
 
ErrMsg: If Err Then MsgBox Err.Description
End Sub

You will notice the red-colored Object-Variable (of type cRPCConnection) -
which resembles in its usage a bit, how one would work with e.g. the WinHTTP 5.1 Object...
Simply put - it encapsulates "the needed Socket-stuff" which is necessary, to be able to
work across machine-boundaries.

After this Object was "set up" (in Form_Load or in Sub Main - or also in a dedicated little
Wrapper-Class), what remains is to look at, where "the RPC-call happens"...
(for simplicity's sake, in this Demo not in an additional WrapperClass, but directly in the Forms: cmdAddTwoLongs_Click()

Just ask yourselves - what will need to happen under the covers of: RPCConn.RPC(...)?
Right (please look at the Strings I've marked blue in the above code):
- to be able to instantiate a Dll regfree from within the serversides \RPCDlls\ folder, we will need the DllName and the ClassName
. (so that we can create an Object-instance, which we will call LateBound then)...
- and to be able to perform a LateBound-Call (per CallByName), we will need the third blue string: "AddTwoLongs" (the Method-name)
- another requirement in the Parameter-List will be a TimeOut-Value (in the above call this is the 4th argument, the '3')
- and then finally the two arguments, which the AddTwoLongs-Method expects at the serverside (a VB6-Param-Array came in handy here)

So that's it basically with regards to a little "How-To-Do RPC-calls the easy way" with the vbRichClient5.

Note, that the RichClient RPC-Classes are in use at hundreds of Client-installations worldwide - and
that these Classes were included from the very beginning of the RichClient-project (over a decade ago).
So, this stuff was quite hardened over the years - and is not a "toy-implementation".

4) One last thing, I'd like to mention still with regards to the Demo (before you run it):

The RPC-Classes support a DebugMode (as contained in the last code-snippet above over: RPCConn.DebugMode = ...)

When this Property is True, then one can do an easy "RoundTrip-Debugging", when the
serverside Dll-Project in question is included in a VB-ProjectGroup.

The Demo will start (without the need to compile any Binaries) per Default in DebugMode -
and an appropriate \SimpleRPC\RPC_Test_Group.vbg File is included in the Root-Folder of the Demo.

Check this mode out first (leaving the DebugMode-CheckBox checked) -
later, when you e.g. have stepped through an RPC-call (per <F8> Key),
you can disable the Debug-Mode - but before you do so, you will have to compile:
- the ServerHost-Project I've mentioned in #1
- the ServerDll-Project I've mentioned in #2 (please make sure, that you compile the Dll into the \RPCDlls\-Folder)
- followed by starting the compiled ServerRPC-Executable
After that you can switch DebugMode Off - and perform "real RPC-calls over sockets"

Here's a ScreenShot of the little Client-App:



Have fun.

Olaf
Attached Files

Dev tool: typedef Converter - Convert C/C++/IDL typedef struct and typedef enum to VB

$
0
0
After spending way too much time doing this manually, this idea came to be. I use this extraordinarily frequently, so thought someone else might one day have a use for it. The title pretty much sums it up; here's some notes:

-Automatically detects if typedef struct or typedef enum
-Types support automatic variable type changing and have the most common ones built in (e.g. DWORD = Long, LPSTR = String)
-Arrays are supported for types, both when defined by number var[10]->var(0 To 9) and by variable, var[MAX_PATH]->var(0 To (MAX_PATH - 1))
-Comments have the option to be included or removed
-Enums that don't have an = sign (sequential) are supported, both with and without an initial entry with =0 or =1
-Option for public or private
-Option to remove 'tag' in names
-Various automatic syntax corrections
-I did leave most string types out of type-replacement, since whether they're used as a String or Long is up to the user; only strings I defined were LPWSTR, LPCWSTR, and LPCTSTR as Long, then LPSTR and BSTR as string.

Samples
typedef enum _tagPSUACTION
{
PSU_DEFAULT = 1 // gets security URL and returns its domain.
,PSU_SECURITY_URL_ONLY // gets just the security URL
} PSUACTION;
Public Enum PSUACTION
PSU_DEFAULT=1 ' gets security URL and returns its domain.
PSU_SECURITY_URL_ONLY = 2 ' gets just the security URL
End Enum
typedef struct SMDATA
{
DWORD dwMask; // SMDM_* values
DWORD dwFlags; // Not used
long hmenu; // Static HMENU portion.
HWND hwnd; // HWND owning the HMENU
UINT uId; // Id of the item in the menu (-1 for menu itself)
UINT uIdParent; // Id of the item spawning this menu
UINT uIdAncestor[80]; // Id of the very top item in the chain of ShellFolders
//IUnknown* punk; // IUnkown of the menuband
long punk; //use pointer??
long pidlFolder;// pidl of the ShellFolder portion
long pidlItem; // pidl of the item in the ShellFolder portion
//IShellFolder* psf; // IShellFolder for the shell folder portion
long psf; //use pointer??
WCHAR pvUserData[MAX_PATH]; // User defined Data associated with a pane.
} SMDATA;
Public Type SMDATA
dwMask As Long ' SMDM_* values
dwFlags As Long ' Not used
hmenu As long ' Static HMENU portion.
hwnd As Long ' HWND owning the HMENU
uId As Long ' Id of the item in the menu (-1 for menu itself)
uIdParent As Long ' Id of the item spawning this menu
uIdAncestor(0 To 79) As Long ' Id of the very top item in the chain of ShellFolders
'IUnknown* punk; // IUnkown of the menuband
punk As long 'use pointer??
pidlFolder As long ' pidl of the ShellFolder portion
pidlItem As long ' pidl of the item in the ShellFolder portion
'IShellFolder* psf; // IShellFolder for the shell folder portion
psf As long 'use pointer??
pvUserData(0 To (MAX_PATH - 1)) As Integer ' User defined Data associated with a pane.
End Type

Those two really show it all...
(the VB output is properly indented, can't see it here)

I might change this into an add-in that could do convert-on-paste or convert from the right click menu, if anyone is interested in that let me know.

NOTE: I believe the people who would use a tool like this would also not need extensive documentation of the code or e.g. not be ok with the only way to add type replacements being to add another line in a function... this isn't for beginners so don't be too harsh about the cryptic code :)
Also, I rely on VB to do things like correct the case of native data types (long isn't replaced with Long), and change &H0001 to &H1; it's not worth doing manually.

If anyone is interested I also have a utility that will turn a UUID into a IID_IWhatever function like the ones in mIID.bas in oleexp.

PS- Don't actually use that SMDATA type; I altered it to show features.
Attached Files

[Vista+] Code Snippet: Get and set the Rating (stars) of a file

$
0
0
In Explorer, things like Pictures and some other types have a 'Rating' property category that shows a 0-5 star rating. You can get and set this rating programmatically, and this also provides a basis for getting and setting other properties. Requires oleexp, v2.0 or higher.

Code:

Public Declare Function ILCreateFromPathW Lib "shell32" (ByVal pwszPath As Long) As Long
Public Declare Sub ILFree Lib "shell32" (ByVal pidl As Long)
Public Declare Function SHCreateItemFromIDList Lib "shell32" (ByVal pidl As Long, riid As UUID, ppv As Any) As Long

Public Function GetFileRating(sFile As String) As Long
'Returns the star rating of a file in number of stars
Dim pidl As Long
Dim isi As IShellItem2
Dim lp As Long
Dim pkRating As PROPERTYKEY '{64440492-4C8B-11D1-8B70-080036B11A03}, 9

DEFINE_PROPERTYKEY pkRating, &H64440492, CInt(&H4C8B), CInt(&H11D1), &H8B, &H70, &H8, &H0, &H36, &HB1, &H1A, &H3, 9

'first, get the shell item representing the file
pidl = ILCreateFromPathW(StrPtr(sFile))
Call SHCreateItemFromIDList(pidl, IID_IShellItem2, isi)

isi.GetUInt32 pkRating, lp 'it's a VT_UI4; 4-byte unsigned integer, which VB's Long can fill in for since a rating can't exceed 99 and be valid

Select Case lp
    Case 1 To 12 'sys default when you assign values in Explorer=1
        lp = 1
    Case 13 To 37 'default=25
        lp = 2
    Case 38 To 62 'default=50
        lp = 3
    Case 63 To 87 'default=75
        lp = 4
    Case 88 To 99 'default=99
        lp = 5
    Case Else
        lp = 0
End Select
GetFileRating = lp
Set isi = Nothing
Call ILFree(pidl) 'always release the memory used by pidls

End Function

Public Function SetFileRating(sFile As String, lNumberOfStars As Long) As Long
'Sets the star rating of a file. Should return 0 if things go ok.
Dim vvar As Variant
Dim lRating As Long
Dim isi As IShellItem2
Dim pidlFile As Long
Dim pps As IPropertyStore
Dim hr As Long
Dim pkRating As PROPERTYKEY '{64440492-4C8B-11D1-8B70-080036B11A03}, 9

DEFINE_PROPERTYKEY pkRating, &H64440492, CInt(&H4C8B), CInt(&H11D1), &H8B, &H70, &H8, &H0, &H36, &HB1, &H1A, &H3, 9

'The rating could technically be anything from 0 to 99; here I use the values that would be used if you set the rating in Explorer
Select Case lNumberOfStars
    Case 1: lRating = 1
    Case 2: lRating = 25
    Case 3: lRating = 50
    Case 4: lRating = 75
    Case 5: lRating = 99
    Case Else: lRating = 0
End Select
vvar = CVar(lRating) 'the property system will expect a PROPVARIANT, but in this case (not all cases), VariantToPropVariant isn't needed, we'll pass vvar directly

'We need the Property Store for the file, which we can get from its IShellItem
pidlFile = ILCreateFromPathW(StrPtr(sFile))
Call SHCreateItemFromIDList(pidlFile, IID_IShellItem2, isi)
   
isi.GetPropertyStore GPS_READWRITE, IID_IPropertyStore, pps 'we need write access- GPS_DEFAULT will not work
 
hr = pps.SetValue(pkRating, vvar) 'returns S_OK if successful
   
If hr = 0 Then
    hr = pps.Commit 'save the changes; returns S_OK if successful
End If

Set pps = Nothing
Set isi = Nothing
Call ILFree(pidlFile) 'always set your pidl free!

SetFileRating = hr
End Function

Public Sub DEFINE_PROPERTYKEY(Name As PROPERTYKEY, L As Long, w1 As Integer, w2 As Integer, B0 As Byte, b1 As Byte, b2 As Byte, B3 As Byte, b4 As Byte, b5 As Byte, b6 As Byte, b7 As Byte, pid As Long)
  With Name.fmtid
    .Data1 = L
    .Data2 = w1
    .Data3 = w2
    .Data4(0) = B0
    .Data4(1) = b1
    .Data4(2) = b2
    .Data4(3) = B3
    .Data4(4) = b4
    .Data4(5) = b5
    .Data4(6) = b6
    .Data4(7) = b7
  End With
  Name.pid = pid
End Sub

If you're not using the mIID.bas from the oleexp thread, also include this:
Code:

Public Sub DEFINE_UUID(Name As UUID, L As Long, w1 As Integer, w2 As Integer, B0 As Byte, b1 As Byte, b2 As Byte, B3 As Byte, b4 As Byte, b5 As Byte, b6 As Byte, b7 As Byte)
  With Name
    .Data1 = L
    .Data2 = w1
    .Data3 = w2
    .Data4(0) = B0
    .Data4(1) = b1
    .Data4(2) = b2
    .Data4(3) = B3
    .Data4(4) = b4
    .Data4(5) = b5
    .Data4(6) = b6
    .Data4(7) = b7
  End With
End Sub
Public Function IID_IShellItem2() As UUID
'7e9fb0d3-919f-4307-ab2e-9b1860310c93
Static iid As UUID
If (iid.Data1 = 0) Then Call DEFINE_UUID(iid, &H7E9FB0D3, CInt(&H919F), CInt(&H4307), &HAB, &H2E, &H9B, &H18, &H60, &H31, &HC, &H93)
IID_IShellItem2 = iid
End Function
Public Function IID_IPropertyStore() As UUID
'DEFINE_GUID(IID_IPropertyStore,0x886d8eeb, 0x8cf2, 0x4446, 0x8d,0x02,0xcd,0xba,0x1d,0xbd,0xcf,0x99);
Static iid As UUID
 If (iid.Data1 = 0) Then Call DEFINE_UUID(iid, &H886D8EEB, CInt(&H8CF2), CInt(&H4446), &H8D, &H2, &HCD, &HBA, &H1D, &HBD, &HCF, &H99)
  IID_IPropertyStore = iid
 
End Function

If you want to display these values in ListView of files, here's a good place to start.

LynxGrid..Modifications

$
0
0
This is a 'rar' file...I just added a '.zip' extension.

The modified LynxGrid that I abandoned for vhGrid might be useful to someone.

Note: the coding is very ugly or basic.

LucasMKG
saying Hala to Jonney and fafalone...for now...:D
Attached Files

[VB6] UserControl Ambient.UserMode workaround

$
0
0
For you usercontrol (UC) creators out there. Everyone else -- won't apply to you.

Ambient.UserMode tells us whether the UC's container is in design mode or user mode/run-time. Unfortunately, this isn't supported in all containers. Word, IE may not report what you expect. Some containers may not implement that property.

VB always implements the Ambient.UserMode property. However, that can be misleading. If you have a UC on a form in design view, UC says Ambient.UserMode = False; great. But if you are creating a new UC and inside that new UC, you add an existing/different UC, that inner UC will report False also; great because this new UC is in design view. Here's the kicker. Now you add that new UC to the form. The inner UC now reports Ambient.UserMode as True, even though the form is in design view

Is this a problem for you? Maybe, only if you are actually testing that property. Let's say you use that property to determine whether or not to start subclassing, whether to start some image animation, maybe start API timers, whatever. You designed your control to not do that if the UC's container is in design view. Works well until your control is placed in another control that is placed on some other container. When your control (compiled or not) is a grandchild, container-wise, it will report Ambient.UserMode as True within VB. Other containers may report different things. The suggestion below allows your customer/user to override and properly set that property.

Let me use a real world example. I designed an image control. That control has a property to begin animation when the UC is in run-time. Well, someone wanted to add my control to a custom UC they were designing. They wanted the animation to occur when their new UC was in run-time. Animation started when their UC was placed on a form in design-time. Not what they wanted. Since my control had a property to start/stop animation, the simple solution was to default not to start animation and also for their UC to check its own Ambient.UserMode and depending on its value, start animation.

This worked well. But what if my UC began doing stuff when its Ambient.UserMode was True, but had no way for the containing control to tell it to stop or don't start at all? That containing control is out of luck.

The following is a workaround that if became a template for all your UCs, you can avoid this problem in any UC you create. Any paying customers for your UC can be educated to the new property and how to use it for their purposes.

Here is a sample of the 'template'. It exposes a Public UserMode property that allows the UC's container to dictate/tell the UC what UserMode it should use. This could be ideal for other non-VB6 containers that either report incorrectly or don't report at all the Ambient.UserMode.

Code:

Public Enum AmbientUserModeENUM
    aumDefault = 0
    aumDesignTime = 1
    aumRuntime = 2
End Enum
Private m_UserMode As AmbientUserModeENUM

Public Property Let UserMode(newVal As AmbientUserModeENUM)
    If Not (newVal < aumDefault Or newVal > aumRuntime) Then
        m_UserMode = newVal
        Call pvCheckUserMode
        PropertyChanged "UserMode"
    End If
End Property
Public Property Get UserMode() As AmbientUserModeENUM
    UserMode = m_UserMode And &HFF
End Property

Private Sub pvCheckUserMode()
    Select Case (m_UserMode And &HFF)
    Case aumDefault
        m_UserMode = (m_UserMode And &HFF) Or UserControl.Ambient.UserMode * &H100&
    Case aumRuntime
        m_UserMode = (m_UserMode And &HFF) Or &H100
    Case Else
        m_UserMode = m_UserMode And &HFF
    End Select
   
    If (m_UserMode And &H100) Then  ' user mode is considered True
        ' do whatever is needed. Maybe set the UserMode property of any child usercontrols

    Else                            ' user mode is considered False
        ' do whatever is needed. Maybe set the UserMode property of any child usercontrols

    End If

End Sub


Private Sub UserControl_InitProperties()
    ' set any new control, initial properties
   
    ' apply any actions needed for UserMode
    Call pvCheckUserMode
End Sub

Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
    ' read all written properties

    ' apply any actions needed for UserMode
    m_UserMode = PropBag.ReadProperty("AUM", aumDefault)
    Call pvCheckUserMode
End Sub

Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
    PropBag.WriteProperty "AUM", (m_UserMode And &HFF), aumDefault
End Sub

Though the call to pvCheckUserMode is placed in Init/ReadProperties, it could be moved to UserControl_Show if desired, depending on your needs.

SHBrowseForFolder: Handling a choice of Libraries (or Library), Computer, or Network

$
0
0
ChooseFolderEx

Project Summary
So if you've ever used a folder choose based on SHBrowseForFolder, you'll notice that most functions that turn its result (a pidl) into a file system path will return nothing, or at best a cryptic string starting with :: (followed by a GUID). But things like Libraries, My Computer, and Network contain folders- and if you're going to be doing something like searching for files, the user may well expect that selecting one of those would search its locations. Thanks to oleexp, the code to find out what those folders are is at least somewhat manageable.

Project Requirements
-At least Windows Vista; Libraries are a Win7+ thing.
-oleexp3.tlb - my fork of olelib with modern interfaces (get it here). This must be added as a reference under Project->References, but doesn't need to be included with a compiled program. No new version was released with this project, so if you already have it you don't need to upgrade this time.



So we begin with calling the Browse API; the wrapper called here is just a standard routine.
Code:

Public Function SelectFolderEx(hWnd As Long, sPrompt As String, dwFlags As BF_Flags, out_Folders() As String, Optional sStartDir As String, Optional sRoot As String) As Long
'Enhanced folder chooser
Dim pidlStart As Long
Dim pidlRoot As Long
Dim lpRes As Long, szRes As String
ReDim out_Folders(0)
If sStartDir <> "" Then
    pidlStart = ILCreateFromPathW(StrPtr(sStartDir))
End If
If sRoot <> "" Then
    pidlRoot = ILCreateFromPathW(StrPtr(sRoot))
End If

lpRes = BrowseDialogEx(hWnd, sPrompt, dwFlags, pidlRoot, pidlStart)
If lpRes = 0 Then
    SelectFolderEx = -1
    Exit Function
End If


szRes = GetPathFromPIDLW(lpRes)
If (szRes = "") Or (szRes = vbNullChar) Then
    'here's where we do some magic. if GetPathFromPIDLW returned nothing, but we did receive
    'a valid pidl, we may have a location that still might be valid. at this time, i've made
    'functions that will return the paths for the Library object, any individual library,
    'My Computer, and the main Network object and network paths
    Dim sAPP As String 'absolute parsing path
    sAPP = GetAbsoluteParsingPath(lpRes)
    If (Left$(sAPP, 2) = "\\") Or (Left$(sAPP, 2) = "//") Then
        'network locations can't be resolved as normal, but are valid locations
        'for most things you'll be passing a folder location too, including FindFirstFile
        out_Folders(0) = sAPP
        SelectFolderEx = 1
        GoTo cfdone
    End If
    SelectFolderEx = EnumSpecialObjectPaths(sAPP, out_Folders)
Else
    out_Folders(0) = szRes
    SelectFolderEx = 1
End If

cfdone:
Call CoTaskMemFree(lpRes)
End Function

The difference here is that instead of giving up and returning a blank or error if we don't get a path path, we're going to check to see if it's an object that does contain file system folders.

The next step is to see which, if any, object we can enumerate:
Code:

Public Function EnumSpecialObjectPaths(szID As String, sPaths() As String) As Long
'objects like Libraries and My Computer can't be passed to a file search algorithm
'but they contain objects which can. this function enumerates the searchable paths
'return value is the count of sPaths, or -1 if the GUID was not an enumerable loc

    If szID = FolderGUID_Computer Then
        'here we can just use the GetLogicalDriveStrings API
        Dim sBuff As String * 255
        Dim i As Long
        i = GetLogicalDriveStrings(255, sBuff)
        sPaths = Split(Left$(sBuff, i - 1), Chr$(0))

    ElseIf (szID = FolderGUID_Libraries) Then 'library master
        ListAllLibraryPaths sPaths
       
    ElseIf (Left$(szID, 41) = FolderGUID_Libraries & "\") Then 'specific library
        ListLibraryPaths szID, sPaths
   
    ElseIf (szID = FolderGUID_Network) Then 'Network master
        ListNetworkLocs sPaths
       
    Else 'not supported or not file system
        EnumSpecialObjectPaths = -1
        Exit Function
    End If

EnumSpecialObjectPaths = UBound(sPaths) + 1

End Function

For My Computer, the job was easy, just had to call the GetLogicalDriveStrings API.
For the rest, we need a more complex enumerator. This is made possible by the fact IShellItem can represent anything, and can enumerate anything, not just normal folders.
There's 2 Library options; if an individual library is selected, that's still not a normal path so has to be handled here- the IShellLibrary interface can tell us which folders are included in the library, so we can go from there. The other is for the main 'Libraries' object being selected- there we get a list of all the libraries on the system (note that we can't just check the standard ones, because custom libraries can be created).
If the Network object is chosen, we filter it down to browseable network paths, since the enum also returns the various non-computer objects that appear there.

Code:

Public Sub ListAllLibraryPaths(sOut() As String)
'Lists all paths in all libraries
Dim psi As IShellItem
Dim piesi As IEnumShellItems
Dim psiLib As IShellItem
Dim isia As IShellItemArray
Dim pLibEnum As IEnumShellItems
Dim pLibChild As IShellItem
Dim lpPath As Long
Dim szPath As String
Dim pLib As ShellLibrary
Set pLib = New ShellLibrary
Dim nPaths As Long
Dim pclt As Long

ReDim sOut(0)

Call SHCreateItemFromParsingName(StrPtr(FolderGUID_Libraries), ByVal 0&, IID_IShellItem, psi)
If (psi Is Nothing) Then
    Debug.Print "could't parse lib master"
    Exit Sub
End If
psi.BindToHandler 0, BHID_EnumItems, IID_IEnumShellItems, piesi

Do While (piesi.Next(1, psiLib, pclt) = S_OK)
    psiLib.GetDisplayName SIGDN_NORMALDISPLAY, lpPath
    szPath = LPWSTRtoStr(lpPath)
    Debug.Print "Enumerating Library " & szPath
    pLib.LoadLibraryFromItem psiLib, STGM_READ
    pLib.GetFolders LFF_ALLITEMS, IID_IShellItemArray, isia
       
    isia.EnumItems pLibEnum

    Do While (pLibEnum.Next(1, pLibChild, 0) = 0)

        pLibChild.GetDisplayName SIGDN_FILESYSPATH, lpPath
        szPath = LPWSTRtoStr(lpPath, True)
        Debug.Print "lib folder->" & szPath
        If Len(szPath) > 2 Then
            ReDim Preserve sOut(nPaths)
            sOut(nPaths) = szPath
            nPaths = nPaths + 1
        End If
        Set pLibChild = Nothing

    Loop
    Set psiLib = Nothing
Loop
End Sub
Public Sub ListLibraryPaths(sPN As String, sOut() As String)
'list the paths of a single library
'sPN is the full parsing name- what is returned from ishellfolder.getdisplayname(SHGDN_FORPARSING)
Dim psiLib As IShellItem
Dim pLib As ShellLibrary
Set pLib = New ShellLibrary
Dim psia As IShellItemArray
Dim pEnum As IEnumShellItems
Dim psiChild As IShellItem
Dim lpPath As Long, szPath As String, nPaths As Long
Dim pclt As Long

Call SHCreateItemFromParsingName(StrPtr(sPN), ByVal 0&, IID_IShellItem, psiLib)
If (psiLib Is Nothing) Then
    Debug.Print "Failed to load library item"
    Exit Sub
End If
pLib.LoadLibraryFromItem psiLib, STGM_READ
pLib.GetFolders LFF_ALLITEMS, IID_IShellItemArray, psia
If (psia Is Nothing) Then
    Debug.Print "Failed to enumerate library"
    Exit Sub
End If

ReDim sOut(0)
psia.EnumItems pEnum

Do While (pEnum.Next(1, psiChild, pclt) = S_OK)
    If (psiChild Is Nothing) = False Then
        psiChild.GetDisplayName SIGDN_FILESYSPATH, lpPath
        szPath = LPWSTRtoStr(lpPath)
        If Len(szPath) > 2 Then
            ReDim Preserve sOut(nPaths)
            sOut(nPaths) = szPath
            nPaths = nPaths + 1
        End If
    End If
    Set psiChild = Nothing
Loop
Set pEnum = Nothing
Set psia = Nothing
Set pLib = Nothing
Set psiLib = Nothing
End Sub
Public Sub ListNetworkLocs(sOut() As String) '
Dim psi As IShellItem
Dim piesi As IEnumShellItems
Dim psiNet As IShellItem
Dim isia As IShellItemArray
Dim pNetEnum As IEnumShellItems
Dim pNetChild As IShellItem
Dim lpPath As Long
Dim szPath As String
Dim nPaths As Long
Dim pclt As Long

Call SHCreateItemFromParsingName(StrPtr(FolderGUID_Network), ByVal 0&, IID_IShellItem, psi)
If psi Is Nothing Then Exit Sub
ReDim sOut(0)
psi.BindToHandler 0, BHID_EnumItems, IID_IEnumShellItems, piesi
Do While (piesi.Next(1, pNetChild, pclt) = S_OK)
    pNetChild.GetDisplayName SIGDN_DESKTOPABSOLUTEPARSING, lpPath
    szPath = LPWSTRtoStr(lpPath)
    If (Left$(szPath, 2) = "//") Or (Left$(szPath, 2) = "\\") Then 'objects besides valid paths come up, like routers, devices, etc
                                    'but they don't start with //, only searchable network locations should
        Debug.Print "netpath " & szPath
        ReDim Preserve sOut(nPaths)
        sOut(nPaths) = szPath
        nPaths = nPaths + 1
    End If
    Set pNetChild = Nothing
Loop
Set piesi = Nothing
Set psi = Nothing
End Sub

The results of this are normal file system paths you can treat like normal results that never returned a blank.

Everything there is designed to support Unicode; but the VB textbox in the sample can't display it. But if you pass the results to something Unicode enabled, like a TextBoxW for example, you'll see the correct names.
Attached Files

SHChangeNotifyRegister updated and corrected, including new delivery method

$
0
0
So there's two reasons why I wanted to post this,
1) The examples on popular sites like VBNet and Brad Martinez's site have several errors, and
2) MSDN states that as of XP and later, all clients should be using a new delivery method that uses shared memory. The only example of this in VB is some obscure, hard to connect to chinese forum posts.

If you're not already familiar with SHChangeNotifyRegister, it allows your program to be notified of any changes to files, folders, and other shell objects. See the SHCNE enum below for the events it has.

Code:

Private Declare Function SHChangeNotifyRegister Lib "shell32" _
                              (ByVal hWnd As Long, _
                              ByVal fSources As SHCNRF, _
                              ByVal fEvents As SHCN_EventIDs, _
                              ByVal wMsg As Long, _
                              ByVal cEntries As Long, _
                              lpps As SHChangeNotifyEntry) As Long

The uFlags argument is not SHCNF values. It's always returned in pidls. SHCNF is for when your program calls SHChangeNotify (I should make a separate thread about that since nobody does that when they should). One of the new SHCNRF values is SHCNRF_NEWDELIVERY, which changes the way you handle the WM_SHNOTIFY message:
Code:

Public Function F1WndProc(ByVal hWnd As Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long, ByVal uIdSubclass As Long, ByVal dwRefData As Long) As Long

    Select Case uMsg

        Case WM_SHNOTIFY
            Dim lEvent As Long
            Dim pInfo As Long
            Dim tInfo As SHNOTIFYSTRUCT
            Dim hNotifyLock As Long
            hNotifyLock = SHChangeNotification_Lock(wParam, lParam, pInfo, lEvent)
            If hNotifyLock Then
                CopyMemory tInfo, ByVal pInfo, LenB(tInfo)
                Form1.HandleNotify tInfo.dwItem1, tInfo.dwItem2, lEvent
                Call SHChangeNotification_Unlock(hNotifyLock)
            End If

Other than demonstrating those changes, it's just a straightforward SHChangeNotifyRegister example that also uses the newer, easier, and safer SetWindowSubclass API for its subclassing.

Requirements
-Windows XP or higher

Code
For quicker implementation, here the full module from the sample; the form just calls start/stop and handles the pidls.
Code:

Option Explicit

Public m_hSHNotify As Long
Public Const WM_SHNOTIFY = &H488 'WM_USER through &H7FF

Public Enum SHCN_EventIDs
  SHCNE_RENAMEITEM = &H1          '(D) A non-folder item has been renamed.
  SHCNE_CREATE = &H2              '(D) A non-folder item has been created.
  SHCNE_DELETE = &H4              '(D) A non-folder item has been deleted.
  SHCNE_MKDIR = &H8              '(D) A folder item has been created.
  SHCNE_RMDIR = &H10              '(D) A folder item has been removed.
  SHCNE_MEDIAINSERTED = &H20      '(G) Storage media has been inserted into a drive.
  SHCNE_MEDIAREMOVED = &H40      '(G) Storage media has been removed from a drive.
  SHCNE_DRIVEREMOVED = &H80      '(G) A drive has been removed.
  SHCNE_DRIVEADD = &H100          '(G) A drive has been added.
  SHCNE_NETSHARE = &H200          'A folder on the local computer is being
                                  '    shared via the network.
  SHCNE_NETUNSHARE = &H400        'A folder on the local computer is no longer
                                  '    being shared via the network.
  SHCNE_ATTRIBUTES = &H800        '(D) The attributes of an item or folder have changed.
  SHCNE_UPDATEDIR = &H1000        '(D) The contents of an existing folder have changed,
                                  '    but the folder still exists and has not been renamed.
  SHCNE_UPDATEITEM = &H2000      '(D) An existing non-folder item has changed, but the
                                  '    item still exists and has not been renamed.
  SHCNE_SERVERDISCONNECT = &H4000 'The computer has disconnected from a server.
  SHCNE_UPDATEIMAGE = &H8000&    '(G) An image in the system image list has changed.
  SHCNE_DRIVEADDGUI = &H10000    '(G) A drive has been added and the shell should
                                  '    create a new window for the drive.
  SHCNE_RENAMEFOLDER = &H20000    '(D) The name of a folder has changed.
  SHCNE_FREESPACE = &H40000      '(G) The amount of free space on a drive has changed.

'#If (WIN32_IE >= &H400) Then
  SHCNE_EXTENDED_EVENT = &H4000000 '(G) Not currently used.
'#End If

  SHCNE_ASSOCCHANGED = &H8000000  '(G) A file type association has changed.
  SHCNE_DISKEVENTS = &H2381F      '(D) Specifies a combination of all of the disk
                                  '    event identifiers.
  SHCNE_GLOBALEVENTS = &HC0581E0  '(G) Specifies a combination of all of the global
                                  '    event identifiers.
  SHCNE_ALLEVENTS = &H7FFFFFFF
  SHCNE_INTERRUPT = &H80000000    'The specified event occurred as a result of a system
                                  'interrupt. It is stripped out before the clients
                                  'of SHCNNotify_ see it.
End Enum

'#If (WIN32_IE >= &H400) Then
  Public Const SHCNEE_ORDERCHANGED = &H2 'dwItem2 is the pidl of the changed folder
'#End If
Public Enum SHCNRF
    SHCNRF_InterruptLevel = &H1
    SHCNRF_ShellLevel = &H2
    SHCNRF_RecursiveInterrupt = &H1000
    SHCNRF_NewDelivery = &H8000&
End Enum


Public Enum SHCN_ItemFlags
  SHCNF_IDLIST = &H0                ' LPITEMIDLIST
  SHCNF_PATHA = &H1              ' path name
  SHCNF_PRINTERA = &H2        ' printer friendly name
  SHCNF_DWORD = &H3            ' DWORD
  SHCNF_PATHW = &H5              ' path name
  SHCNF_PRINTERW = &H6        ' printer friendly name
  SHCNF_TYPE = &HFF
  ' Flushes the system event buffer. The function does not return until the system is
  ' finished processing the given event.
  SHCNF_FLUSH = &H1000
  ' Flushes the system event buffer. The function returns immediately regardless of
  ' whether the system is finished processing the given event.
  SHCNF_FLUSHNOWAIT = &H2000

'I prefer to always specify A or W, but you can also do it the way previous examples have
' (but this doesn't apply to SHChangeNotifyRegister, just SHChangeNotify, not covered here)
'#If UNICODE Then
'  SHCNF_PATH = SHCNF_PATHW
'  SHCNF_PRINTER = SHCNF_PRINTERW
'#Else
'  SHCNF_PATH = SHCNF_PATHA
'  SHCNF_PRINTER = SHCNF_PRINTERA
'#End If
End Enum



Private Type SHNOTIFYSTRUCT
  dwItem1 As Long
  dwItem2 As Long
End Type

Private Type SHChangeNotifyEntry
  ' Fully qualified pidl (relative to the desktop folder) of the folder to monitor changes in.
  ' 0 can also be specifed for the desktop folder.
  pidl As Long
  ' Value specifying whether changes in the folder's subfolders trigger a change notification
  '  event (it's actually a Boolean, but we'll go Long because of VB's DWORD struct alignment).
  fRecursive As Long
End Type

Private Declare Function SHChangeNotifyRegister Lib "shell32" _
                              (ByVal hWnd As Long, _
                              ByVal fSources As SHCNRF, _
                              ByVal fEvents As SHCN_EventIDs, _
                              ByVal wMsg As Long, _
                              ByVal cEntries As Long, _
                              lpps As SHChangeNotifyEntry) As Long

Private Declare Function SHChangeNotifyDeregister Lib "shell32" Alias "#4" (ByVal hNotify As Long) As Boolean

Private Declare Function SHChangeNotification_Lock Lib "shell32" (ByVal hChange As Long, _
                                                                ByVal dwProcId As Long, _
                                                                pppidl As Long, _
                                                                plEvent As Long) As Long
                                                               
Private Declare Function SHChangeNotification_Unlock Lib "shell32" (ByVal hLock As Long) As Long
Private Declare Function SHGetPathFromIDListW Lib "shell32.dll" (ByVal pidl As Long, ByVal pszPath As Long) As Long
Public Declare Function SHGetSpecialFolderLocation Lib "shell32.dll" (ByVal hWndOwner As Long, ByVal nFolder As SHSpecialFolderIDs, pidl As Long) As Long
Public Enum SHSpecialFolderIDs
    'See full project or somewhere else for the full enum, including it all ran over the post length limit
    CSIDL_DESKTOP = &H0

End Enum

Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)

Private Declare Function DefSubclassProc Lib "comctl32.dll" Alias "#413" (ByVal hWnd As Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
Private Declare Function SetWindowSubclass Lib "comctl32.dll" Alias "#410" (ByVal hWnd As Long, ByVal pfnSubclass As Long, ByVal uIdSubclass As Long, Optional ByVal dwRefData As Long) As Long
Private Declare Function RemoveWindowSubclass Lib "comctl32.dll" Alias "#412" (ByVal hWnd As Long, ByVal pfnSubclass As Long, ByVal uIdSubclass As Long) As Long
Private Const WM_DESTROY = &H2
Public Const MAX_PATH = 260

Public Function StartNotify(hWnd As Long, Optional pidlPath As Long = 0) As Long
  Dim tCNE As SHChangeNotifyEntry
  Dim pidl As Long
 
  If (m_hSHNotify = 0) Then
        If pidlPath = 0 Then
            tCNE.pidl = VarPtr(0) 'This is a shortcut for the desktop pidl (to watch all locations)
                                  'only use this shortcut as a one-off reference immediately passed
                                  'to an API and not used again
        Else
            tCNE.pidl = pidlPath 'You can specify any other fully qualified pidl to watch only that folder
                                'Use ILCreateFromPathW(StrPtr(path))
        End If
      tCNE.fRecursive = 1
     
      'instead of SHCNE_ALLEVENTS you could choose to only monitor specific ones
      m_hSHNotify = SHChangeNotifyRegister(hWnd, SHCNRF_ShellLevel Or SHCNRF_InterruptLevel Or SHCNRF_NewDelivery, SHCNE_ALLEVENTS Or SHCNE_INTERRUPT, WM_SHNOTIFY, 1, tCNE)
     
     
      StartNotify = m_hSHNotify
       
  End If  ' (m_hSHNotify = 0)

End Function
Public Function StopNotify() As Boolean
StopNotify = SHChangeNotifyDeregister(m_hSHNotify)
End Function
Public Function LookUpSHCNE(uMsg As Long) As String

Select Case uMsg

Case &H1: LookUpSHCNE = "SHCNE_RENAMEITEM"
Case &H2: LookUpSHCNE = "SHCNE_CREATE"
Case &H4: LookUpSHCNE = "SHCNE_DELETE"
Case &H8: LookUpSHCNE = "SHCNE_MKDIR"
Case &H10: LookUpSHCNE = "SHCNE_RMDIR"
Case &H20: LookUpSHCNE = "SHCNE_MEDIAINSERTED"
Case &H40: LookUpSHCNE = "SHCNE_MEDIAREMOVED"
Case &H80: LookUpSHCNE = "SHCNE_DRIVEREMOVED"
Case &H100: LookUpSHCNE = "SHCNE_DRIVEADD"
Case &H200: LookUpSHCNE = "SHCNE_NETSHARE"
Case &H400: LookUpSHCNE = "SHCNE_NETUNSHARE"
Case &H800: LookUpSHCNE = "SHCNE_ATTRIBUTES"
Case &H1000: LookUpSHCNE = "SHCNE_UPDATEDIR"
Case &H2000: LookUpSHCNE = "SHCNE_UPDATEITEM"
Case &H4000: LookUpSHCNE = "SHCNE_SERVERDISCONNECT"
Case &H8000&: LookUpSHCNE = "SHCNE_UPDATEIMAGE"
Case &H10000: LookUpSHCNE = "SHCNE_DRIVEADDGUI"
Case &H20000: LookUpSHCNE = "SHCNE_RENAMEFOLDER"
Case &H40000: LookUpSHCNE = "SHCNE_FREESPACE"
Case &H4000000: LookUpSHCNE = "SHCNE_EXTENDED_EVENT"
Case &H8000000: LookUpSHCNE = "SHCNE_ASSOCCHANGED"
Case &H2381F: LookUpSHCNE = "SHCNE_DISKEVENTS"
Case &HC0581E0: LookUpSHCNE = "SHCNE_GLOBALEVENTS"
Case &H7FFFFFFF: LookUpSHCNE = "SHCNE_ALLEVENTS"
Case &H80000000: LookUpSHCNE = "SHCNE_INTERRUPT"

End Select
End Function
Public Function GetPathFromPIDLW(pidl As Long) As String
  Dim pszPath As String
  pszPath = String(MAX_PATH, 0)
  If SHGetPathFromIDListW(pidl, StrPtr(pszPath)) Then
    If InStr(pszPath, vbNullChar) Then
        GetPathFromPIDLW = Left$(pszPath, InStr(pszPath, vbNullChar) - 1)
    End If
  End If
End Function
Public Function Subclass(hWnd As Long, lpfn As Long, Optional uId As Long = 0&, Optional dwRefData As Long = 0&) As Boolean
If uId = 0 Then uId = hWnd
    Subclass = SetWindowSubclass(hWnd, lpfn, uId, dwRefData):      Debug.Assert Subclass
End Function

Public Function UnSubclass(hWnd As Long, ByVal lpfn As Long, pid As Long) As Boolean
    UnSubclass = RemoveWindowSubclass(hWnd, lpfn, pid)
End Function
Public Function FARPROC(pfn As Long) As Long
  FARPROC = pfn
End Function

Public Function F1WndProc(ByVal hWnd As Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long, ByVal uIdSubclass As Long, ByVal dwRefData As Long) As Long

    Select Case uMsg

        Case WM_SHNOTIFY
            Dim lEvent As Long
            Dim pInfo As Long
            Dim tInfo As SHNOTIFYSTRUCT
            Dim hNotifyLock As Long
            hNotifyLock = SHChangeNotification_Lock(wParam, lParam, pInfo, lEvent)
            If hNotifyLock Then
                CopyMemory tInfo, ByVal pInfo, LenB(tInfo)
                Form1.HandleNotify tInfo.dwItem1, tInfo.dwItem2, lEvent
                Call SHChangeNotification_Unlock(hNotifyLock)
            End If

      Case WM_DESTROY
     
        Call UnSubclass(hWnd, PtrF1WndProc, uIdSubclass)
        'Exit Function
  End Select
 
  ' Pass back to default message handler.

      F1WndProc = DefSubclassProc(hWnd, uMsg, wParam, lParam)


Exit Function

End Function
Private Function PtrF1WndProc() As Long
PtrF1WndProc = FARPROC(AddressOf F1WndProc)
End Function

The form is just the start/stop buttons and a list:
Code:

Option Explicit

Public Function HandleNotify(dwItem1 As Long, dwItem2 As Long, idEvent As Long) As Long
Dim sArg1 As String, sArg2 As String
If dwItem1 Then
    sArg1 = GetPathFromPIDLW(dwItem1)
End If
If dwItem2 Then
    sArg2 = GetPathFromPIDLW(dwItem2)
End If
Dim sEvent As String
sEvent = LookUpSHCNE(idEvent)

List1.AddItem sEvent & ", Item1=" & sArg1 & ", Item2=" & sArg2


End Function

Private Sub cmdStart_Click()
StartNotify Me.hWnd
End Sub

Private Sub cmdStop_Click()
StopNotify
End Sub

Private Sub Form_Load()
Subclass Me.hWnd, AddressOf F1WndProc
End Sub

Private Sub Form_Unload(Cancel As Integer)
StopNotify
End Sub

Private Sub Form_Resize()
On Error Resume Next
List1.Width = Me.Width - 220
List1.Height = Me.Height - 1000
End Sub

Attached Files

[vb6] Extending VB's Image Control for PNG, TIFF, and more [v1.1]

$
0
0
UPDATED on 11 Oct 2015. Major revision. Added animated GIF and mutlipage TIFF support.
Any previous version must be thrown away. You do not want to run both in the same project, trust me!
12 Oct 2015: Added more robust method of activating images at runtime from controls loaded at design-time. See post #11 for more

The VB Image control is now more flexible. Want to display PNG and TIFF without using a 3rd party control? It can do that. Want to display 32 bit alpha-blended bitmaps? It can do that. Want to animate GIFs and display nice 32 bit alpha-blended icons? Now it can ;)

What's even more amazing is that this isn't limited to the image control. These images can be displayed in most things with a Picture property -- not every Picture property, but most. Want to add a nice PNG to a button, yep it can be done. Regardless, true transparency is maintained, nothing faked. Change the backcolor of some container, and you don't have to reload the image to fake transparency again. Someone changes themes on the pc & it doesn't effect the transparency.

Major Limitations:
1. Cannot see transparency in bitmaps while in design-view.
2. Cannot load PNG,TIFF while in design-view

How this class works.
1. Converts non-icon/gif/metafile formats to 32bpp premultiplied RGB bitmap if image contains transparency. AlphaBlend API is used to render the image in that case
2. Icons/Cursors are loaded as-is and rendered with DrawIconEx API if icon/cursor is 32bpp
3. GDI+ is used to load PNG/TIF/Animated GIF and convert to 32bpp or 24bpp bitmaps for display
4. Only images that need special rendering are custom drawn
5. Subclassing of the picture is performed for two reasons: a) handle rendering as just described and b) tell VB that a picture contains transparency so VB will repaint behind the image before it's drawn.

This version of the class has a few active methods.

1. SetImage. This is a replacement for VB's LoadImage and Set methods that pertain to images. It allows unicode filenames and can support TIF, PNG, 32bpp bitmaps & icons/cursors, PNG-encoded Vista-style icons, plus what VB supports.

2. SetSubImage. If a multipage TIFF or animated GIF has been loaded with option chosen for navigation, this method changes the page/frame.

3. WantEvents. Method allows you to receive an event to enable you to draw behind or on top of any image

4. EnableSubclassing. Allows you to turn off subclassing. By default, subclassing is enabled. But since subclassing in IDE is not safe, it is recommended you disable subclassing when working on your project. Turn it on when you want to view the images with transparency for a quick look-see. Then close your project normally and disable subclassing as desired. Subclassing cannot be disabled when project is compiled. Once disabled during IDE run-time, cannot be re-enabled until project closes -- safety measure.

5. SubImageCount. Returns number of frames/pages if animated GIF or multipage TIFF loaded

6. GetGIFAnimationInfo. Returns the frame duration and loop count for animated GIFs

I have included a very simple sample project. The project's images you see in design view are one that is premultiplied and one that is not. Both are 32 bit bitmaps that use the alpha channel.

Screenshot below are two VB image controls. You can clearly see they are drawing blended alpha channels. Background is transparent, soft edges and shadows. The updated zip below does contain an animated GIF example.

Name:  Untitled.jpg
Views: 68
Size:  36.6 KB

Suggestion: GIF/TIFF animation/navigation is not disabled when EnableSubclassing = False. I feel it should be. GDI+ can cause crashes too in IDE if it is not shut down properly and hitting END prevents proper shutdown. If you agree, you will want to make this modification:

Routine: SetImage
Add the following before the line: Select Case PropertyName
Code:

If Me.EnableSubclassing = False Then RequiredImageType = RequiredImageType And Not lifAllowNavigation
Attached Images
 
Attached Files

VBIDEUtils now open sources

$
0
0
Hi

I wrote very very long time ago (in 1999) the best VB6 addin : VBIDEUtils (You still can find it on a lot of web sites)
In the past I was selling it, but as I had shutdown VBDiamond 10 years ago, I haven't sell it anymore
I use it off course everyday in all my VB6 projects, to clean code, indent, add automatic error handling, optimize code...
It does better job than MZtools, and also far more other things.

Here is a small description.
VBIDEUtils is a great add-in for VB 5.0 and VB 6.0. With
this add-in, you can do :
- Code Repository
- Store VB Code, Classes, Projects
- Store files
- Store HTML pages
- Store HTML links
- Search through all the database
- Store VB Scripts
- Java Scripts
- Java
- Icons
- Use bookmarks
- Enhanced search
- Search through all the VB Web sites
- Synchronize your local DB with the DB of VBDiamond
- Synchronize code with the major VB Code sites
- Search for programming books on the Web
- Save your code as HTML pages
-
- .....
- Make search through a very extensive Book repository
- Indent easily your code, procedure, block, variables
- Add comment to your modules and procedures
- Find the corresponding ending block instruction
- Know all the APIs error name
- Clear the debug Window
- Change easily the taborder of all your controls
- Add customized error handler to your code
- Add enhanced error handler to your code with loggin, trace
- Show the KeyAscii table to help you coding functions
- An assistant to create your messagebox
- A Toolbar code generator
- Close all the unused windows in the VB IDE
- Spy the classname of each windows
- An Icon browser
- A DBCreator code generator
- An ActiveX documentor
- Export all code to HTML files (from the VB IDE or the VBCodedatabase)
- Import/Export to VCL and DCB files
- Export the VB Code from the VB IDE to HTML files
- Extract all the strings and translate them directly in the addin for further use of resources String and so internationalize your applications
- Change/Modify the tooltips all your controls
- Add new procedure/functions/properties easily with parameters, description....
- Get all dependencies of an executable or a VB project
- Analyze VB projects
- Search all the web in the VB sites directly from the addin
- Added a VB project explorer
- Profile your VB projects and detect dead code including dead variables, dead procedures...
- Add/remove line numbering in your code
- Control properties assistant
- Accelerator assistant
- Generate code to create toolbar at runtime
- Get easily code from several VB Code websites on the web
- Added an enhanced find in VB projects
- Added the automatic creation of connections strings for ADO
- Generate automatically DLL Base Adresses
- Generate GUID
- A lot of of other new features

I decided to release the sources in order to add new possibilities with the community here, and eventually, why not doing a MS Access version compatible, and a .NET Version.
I made a .NET version for the first version of .NET (very long time ago, in 2010), but due to a lack of time, I stopped it.

You will have certainly to compile it, and just call the function "AddToINI" to add it to your VB6 Addins list as I removed all the install part

So here are the sources, of VBIDEUtils.
Please, don't forget, it has bee written in 1999, so 15 years ago, and of course, if I had to rewrite it now, I will use other coding way for many things.
Also, some parts of the code are not used anymore, but, this is normal for a such old project.

If you add functionalities, please post them here it order to make it even better, and offer other to other VB Coder.

Otherwise, you can us the code in your own project, and if like VBIDEUtils or the code, just say hello to your neighbors and all people in the street, in real life, as there is a big lack of real life those days.

Enjoy.

Well, I tried to upload the ZIP with the whole sources, but it more than 2Mb.
I try to find a way

Well, the size of the attachment is limited to 500K
So if a moderator could do something for me?

In the meantime, here is a link : https://github.com/tannerhelland/VBIDEUtils

[VB6] Locale Sensitive Sorting

$
0
0
There are times when you need to sort in a locale-aware manner.

One of the more obvious cases is probably when generating cryptographic signatures for web services. These often require you to create a hash-based message authentication code (HMAC) based on inputs including a canonicalized URI, several HTTP header values including a timestamp, a secret key, and perhaps other items. These items normally have to be sorted so that the server end can reproduce the same HMAC by calculation, and that means both ends have to agree on the collating sequence.

Often you can get away with a lot because most of the characters are going to fall within the 7-bit ASCII range. But when they don't you need to be sure you are using the "invariant, string-oriented" collating sequence and not your user session collating sequence or one that takes language quirks into account.

Many HMAC sigs require that you hash the UTF-8 too, but it works if you first sort UTF-16LE Unicode and then re-encode as UTF-8 (same sequence).

And of course sorting gets used all over - though most uses aren't as sensitive as crypto processes can be.


Subtleties

Accented characters may sort earlier or later depending on the language. Ligatures (e.g. mediæval vs. mediaeval) need to be considered. "String compare" and "linguistic compare" differ. And on and on it goes.


Demo

This demo uses a simplistic Insertion Sort. This is quick and dirty, understood by most sorting fans, and importantly it is a stable sort so it will help showcase my point here.

Basically there is nothing special about it except that it uses CompareStringEx() in Kernel32.dll to compare strings within the sort. For those still using the unsupported Windows XP or earlier you may have to hack it a bit to make use of the aging CompareString() instead.

While the new entrypoint accepts locale string values instead of LCIDs, it may be worth noting that the older one comes in both ANSI and Unicode flavors.

The demo includes a sample list of string data as a Unicode text file. You can modify this with interesting cases you may know of. It has a brief list of "western" languages. You can add or remove values to that list within the code ot change the program to load them from a file too.

The list is loaded up and displayed in a flexgrid with back-colors from white through deepening greenish-blue shades that help make sorting differences easier to see when you try various collating sequence modifications. Because of the not-so-clever way this is done a string list of more than 255 elements will crash the program. ;)

Name:  sshot.png
Views: 113
Size:  25.4 KB


Requirements

VB6, because VB6 comes with MSHFlexgrid which is Unicode-aware. VB5 will work if you substitute another Unicode grid or use the crusty old MSFlexgrid and avoid "invalid in your locale's ANSI" characters.

Windows Vista or later, because of the new CompareStringEx() used here. If you modify the program to call CopmareSting() instead it works on downlevel unsupported Windows versions but you can't use locale strings and will have to change the pick list to use LCID values instead.

Sticking with Unicode support means "eastern" languages can be tested too.


Running the Demo

Nothing special required, and it should just unzip, open, and run even without compiling to EXE first. MSHFlexgrid comes with VB6 so you're set. VB5 users see Requirements section above.

Click the "Sort" button. Change the settings and "Sort" again. Scroll through the list of interesting cases - the scroll position should be stable between "Sorts" so look at the O'Leary case and flip sorting between "String Sort" and "Linguistic Sort" (i.e. "String Sort" not chosen). Ancien Régime is another interesting case.
Attached Images
 
Attached Files

[VB6] Use System's Format PropPage Dialog at Run Time

$
0
0
I've never needed this but a question here got me thinking it couldn't be that hard. Then I started searching the MSDN Library and found nothing I recognized as helpful. Then I searched the web and was shocked to find almost nothing at all.

Finally I found a forum post at another site that led to me Edanmo's old VB6 archives, and a breadcrumb of information there. That was great until I realized how little it covered... such as how to apply the results once you'd managed to raise the dialog in the first place!

As far as I can tell you're pretty much going to need UserControls to implement such features because I can't figure out how to get VB to let you "host" Property Page dialogs in a Form. But this is just a working sample to get you started, and there may be lots for you to discover once you begin fiddling with it.


Requirements

VB6 of course. This might also be converted to work in VB5 but I can't be sure since I haven't tried to.

Any 32- or 64-bit Windows versions that supports VB6 programs.

Microsoft Data Formatting Object Library 6.0 (SP6), i.e. msstdfmt.dll, which comes with VB6 (older service pack versions may possibly be compatible). This needs to be deployed since it isn't among the bits Microsoft ships as part of Windows these days.


What we have here

The demo package attached includes a UserControl that I have named "FmtTextBox" which is basically wrapping a MultiLine = False intrinsic TextBox, a clickable Image control "icon/button" of sorts, and a Variant. The idea is that instead of text, this control's value property (cleverly named "Value") can be any simple data type, and the visible/editable text is parsed-into/formatted-from this Variant Value.

So this makes a sort of non-bindable "TextBox" that handles formatting of many Variant subtypes... and lets the user change the format at run time.

There is also a helper Class that I have named "SettingsManager" designed to assist the program in persisting and restoring these settings between runs of the program.

Then there's the Standard EXE project with one Form that demonstrates the items above.


Running the Demo

Just unzip the attached archive and open the .VBP file in the VB6 IDE via Explorer. You can run it there or compile it first.

I have built "FmtTextBox" to hide its "edit the format button/icon" until the program toggles this on. In the demo a check/uncheck menu item controls this. Here's a peek:


Name:  sshot1.png
Views: 97
Size:  17.1 KB

The menu controlling the "edit formatting" button


Name:  sshot2.png
Views: 83
Size:  16.9 KB

User can click here to open the Property Page dialog


Name:  sshot3.png
Views: 79
Size:  22.0 KB

The Property Page dialog


Whew

This was a lot more than I bargained for when I started it. Partly because a UserControl was needed but more so because using a UserControl fronting a Variant added complexity, and mostly because there was a ton of "guess then cut-and-try work" involved in figuring out how to make use of the PPG dialog once I could get it to show up!

No claims this is bug-free. Consider it a technique demonstration. I'm not sure how practical it might be to do for other controls, but perhaps that isn't needed as much for most controls. As it is I've never needed any of this myself.

But it sure killed some time waiting for phone calls and such.
Attached Images
   
Attached Files

[VB6] clsCursor - Setting the IDC_HAND & Other Cursors Properly

$
0
0
Most solutions that addresses the MousePointer property's lack of support for some of the standard cursors (most notably the "hand" cursor) tend to be based on either converting the standard cursor to a MouseIcon/DragIcon or setting the cursor using the SetCursor API function during the MouseMove event. While both approaches produce generally acceptable results most of the time, they still have obvious shortcomings that makes them appear like cheap workarounds. Converting a standard cursor to a MouseIcon/DragIcon, for instance, doesn't support animated cursors. Setting the cursor during the MouseMove event, on the other hand, exhibits an annoying flickering as the cursor rapidly alternates between the class cursor and the specified cursor. The proper way of dealing with this, according to MSDN, is through subclassing:

Quote:

Originally Posted by MSDN
The Window Class Cursor

When you register a window class, using the RegisterClass function, you can assign it a default cursor, known as the class cursor. After the application registers the window class, each window of that class has the specified class cursor.

To override the class cursor, process the WM_SETCURSOR message. You can also replace a class cursor by using the SetClassLong function. This function changes the default window settings for all windows of a specified class. For more information, see Class Cursor.

The small and simple class module (and supporting standard module) in the attachment below contains all of the logic needed to set the desired standard cursor for all of the specified windowed and/or windowless controls. A demo project is also included that illustrates its use.


Name:  clsCursor Demo.png
Views: 96
Size:  5.8 KB


Subclassing, of course, has its disadvantages as well, especially when debugging in the IDE. However, for those seeking more professional looking results, there's no better way of overriding the class cursor than via subclassing.
Attached Images
 
Attached Files

VB6 in AppServer-scenarios (DCOM Replacement per RC5)

$
0
0
In the early days of VB6-usage there was DCOM (later superseded by COM+).

It came with the promise of easy cross-machine-calls (RPCs) by simply using the second
(optional) Parameter [ServerName] of the CreateObject-call...

Now, is there anybody out there (aside from myself), who ever used that (or anybody who's still using it)?
I guess not - and there's a reason for it.

Don't get me wrong - DCOM/COM+ is a great technology - which still works to this day -
*but* - for proper usage you will have to study a few books about that topic, before you
make your first serious steps ... -> right into "config-hell".

So, basically "nice stuff" (and used to this day in some LAN-scenarios, after a "config-orgy"
and countless Proxy-installs on the clients) - but firing it up as easily as the CreateObject-call
suggests? ... Forget about it.

Well, the RichClient5 offers an alternative to DCOM/COM+, which in contrast supports:
- not touching the Registry (serverside Dlls don't need to be registered)
- avoidance of clientside Proxy-installs (to match the interfaces of the serverside COM-Dlls)
- easy movement of the RC5-RPC serverside part to a different Machine per X-Copy of the Server-RootFolder
- same performance as DCOM/COM+ (thousands of Remote-Requests per second in multiple WorkerThreads)
. but using only a single Port ... whereas DCOM/COM+ needs a complete Port-Range
- usable therefore also in Internet-Scenarios, also due to strong authentication/encryption and built-in compression

Ok, so where's the beef - how to use that thing?

Here's the Code for a SimpleRPC-Demo Attachment 130681 ...
and a short description with some background follows below...

A finished solution consists of three things (three VB6-Projects):


VB-Project #1: The Server-Application (providing the HostProcess for the AppServer-Listener)
- in the above Zip, this is the Project sitting in Path: ..\RPCServer\RPCServer.vbp

This is the most easy of the three parts, since it is not "ClientApp- or Server-Dll specific" -
just a hosting Exe-Project for the Service which will work with any ServerDll and any Client.

You will only have to compile it once - and can then forget about it...

Here's the complete SourceCode for this ServerHost-Executable (all in a little Form):
Code:

Private RPCListener As cRPCListener 'define the RPC-Server-Listener
Private IP As String, Port As Long, DllPath As String 'Start-Parameters

Private Sub Form_Load()
  'normally this part is contained in a Windows-Service-Executable (without any UI)
 
  IP = New_c.TCPServer.GetIP("")      'get the default-IP of the current machine
  Port = 22222                        'set a Port (22222 is the RC5-RPC default-port)
  DllPath = App.Path & "\RPCDlls\"  'Path, where the Server is looking for the RPCDlls
 
  Set RPCListener = New_c.RPCListener 'create the RPC-Listener-instance
 
  If RPCListener.StartServer(IP, Port, , , , , DllPath) Then '... now we try to start the RPC-Server
    Caption = "Server is listening on: " & IP & ":" & Port
  Else
    Caption = "Server-Start was not successful"
  End If
End Sub

Private Sub Form_Terminate()
  If Forms.Count = 0 Then New_c.CleanupRichClientDll
End Sub

That's it with regards to the ServerHost-instance (a normal UserMode-Executable in our Demo-case).


VB-Project(s) #2: One (or more) ActiveX-Server-Dll(s)
- in the above Zip, this is the Project sitting in Path: ..\RPCServer\RPCDlls\SimpleServerLib.vbp

When you look at the above code for the Service-Host - and its RPCListener.StartServer-function, you will see that it receives a
StartParameter 'DllPath' which in this case points to a SubFolder of the Serverhost-Executable: App.Path & "\RPCDlls\"

And this place (this RPCDlls-Folder) is, where you will have to put your compiled Server-Dlls into.
The Public Subs and Functions you will put into the Class(es) of these Dlls will be, what you later on call remotely
(without the need to register these Dlls).

Here's the whole code of the single Class (cServerClass), this Dll-Project contains -
and yes, you can write this code as any other VB6-Code, as normal Public Subs and Functions
(this little Dll-Project doesn't even have a reference to vbRichClient5, the only reference it contains,
is the one to "ADO 2.5", since it will transfer an ADO-Recordset back to the clientside later on).

Code:

Private Cnn As ADODB.Connection
 
Public Function StringReflection(S As String) As String
  StringReflection = StrReverse(S)
End Function

Public Function AddTwoLongs(ByVal L1 As Long, ByVal L2 As Long) As Long
  AddTwoLongs = L1 + L2
End Function

Public Function GetADORs(SQL As String) As ADODB.Recordset
  If Cnn Is Nothing Then OpenCnn
  Set GetADORs = New ADODB.Recordset
      GetADORs.Open SQL, Cnn, adOpenStatic, adLockBatchOptimistic 'return the ADO-Rs (its content will be auto-serialized)
End Function

Private Sub OpenCnn()
  Set Cnn = New Connection
      Cnn.CursorLocation = adUseClient
      Cnn.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & App.Path & "\Test.mdb"
End Sub

That's it - nothing more is needed for the "active part" of the serverside (the Server-Dlls).
The serverside code is hereby (with #1 and #2) completely finished!


VB-Project #3: The Client-App
- in the above Zip, this is the Project sitting in Path: ..\ClientApp\SimpleRPC.vbp

What remains now, is the clientside part of the RPC - the one which *initiates* an
RPC-(Remote-Procedure-call).

The behaviour (to make the program-flow easier) is in case of the RC5-RPCs *always*
synchronously. That means, that RPCs will not return, until we got a Result, or an
Error-message - or a TimeOut-Error back from such a Remote-Method-call against the Server.

Although also the Clientside-Code is not more than 50 lines or so, I will put only
this smaller excerpt of the client-sides Form-code here into a code-section to explain...:

Code:

Private Const ServerDll$ = "SimpleServerLib.dll" 'Name of the used Dll in the \RPCDlls\-Folder
Private Const ServerCls$ = "cServerClass" 'Name of the Class, which is contained in above Dll
 
Private RPCConn As cRPCConnection 'define the Var for the clientside RPC-connection
 
Private Sub Form_Load()
  Set RPCConn = New_c.RPCConnection 'create the clientside-RPCConnection-instance
      RPCConn.DebugMode = (chkDEBUGMode.Value = vbChecked) 'Debug-Mode (should be switched Off when running as an Executable)
      RPCConn.Host = ""        'put an explicit Server-IP here later on, e.g. read from an Ini-File
      RPCConn.Port = 22222    'Port-Nr the Server is listening on (22222 is the RC5-RPC-default)
      RPCConn.KeepAlive = True 'set KeepAlive for better performance
End Sub

'... snipped the two other Methods, which we also wrap in this Form

Private Sub cmdAddTwoLongs_Click() 'an example Remote-Method-Call
On Error GoTo ErrMsg
 
  txtAdd.Text = RPCConn.RPC(ServerDll, ServerCls, "AddTwoLongs", 3, _
                CLng(txtL1.Text), CLng(txtL2.Text)) '<- Parameter-List (two Long-Values in this case)
 
ErrMsg: If Err Then MsgBox Err.Description
End Sub

You will notice the red-colored Object-Variable (of type cRPCConnection) -
which resembles in its usage a bit, how one would work with e.g. the WinHTTP 5.1 Object...
Simply put - it encapsulates "the needed Socket-stuff" which is necessary, to be able to
work across machine-boundaries.

After this Object was "set up" (in Form_Load or in Sub Main - or also in a dedicated little
Wrapper-Class), what remains is to look at, where "the RPC-call happens"...
(for simplicity's sake, in this Demo not in an additional WrapperClass, but directly in the Forms: cmdAddTwoLongs_Click()

Just ask yourselves - what will need to happen under the covers of: RPCConn.RPC(...)?
Right (please look at the Strings I've marked blue in the above code):
- to be able to instantiate a Dll regfree from within the serversides \RPCDlls\ folder, we will need the DllName and the ClassName
. (so that we can create an Object-instance, which we will call LateBound then)...
- and to be able to perform a LateBound-Call (per CallByName), we will need the third blue string: "AddTwoLongs" (the Method-name)
- another requirement in the Parameter-List will be a TimeOut-Value (in the above call this is the 4th argument, the '3')
- and then finally the two arguments, which the AddTwoLongs-Method expects at the serverside (a VB6-Param-Array came in handy here)

So that's it basically with regards to a little "How-To-Do RPC-calls the easy way" with the vbRichClient5.

Note, that the RichClient RPC-Classes are in use at hundreds of Client-installations worldwide - and
that these Classes were included from the very beginning of the RichClient-project (over a decade ago).
So, this stuff was quite hardened over the years - and is not a "toy-implementation".

4) One last thing, I'd like to mention still with regards to the Demo (before you run it):

The RPC-Classes support a DebugMode (as contained in the last code-snippet above over: RPCConn.DebugMode = ...)

When this Property is True, then one can do an easy "RoundTrip-Debugging", when the
serverside Dll-Project in question is included in a VB-ProjectGroup.

The Demo will start (without the need to compile any Binaries) per Default in DebugMode -
and an appropriate \SimpleRPC\RPC_Test_Group.vbg File is included in the Root-Folder of the Demo.

Check this mode out first (leaving the DebugMode-CheckBox checked) -
later, when you e.g. have stepped through an RPC-call (per <F8> Key),
you can disable the Debug-Mode - but before you do so, you will have to compile:
- the ServerHost-Project I've mentioned in #1
- the ServerDll-Project I've mentioned in #2 (please make sure, that you compile the Dll into the \RPCDlls\-Folder)
- followed by starting the compiled ServerRPC-Executable
After that you can switch DebugMode Off - and perform "real RPC-calls over sockets"

Here's a ScreenShot of the little Client-App:



Have fun.

Olaf
Viewing all 1540 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>