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

RTF Label & RTF Button

$
0
0
The attached project contains an RTF Label and an RTF Button control.

I use these all the time, but I haven't messed with the code in either of them in years.

There are quite possibly better options around, but someone asked for these so I posted them.

One nice feature is that you edit the captions directly over the Label or Button (rather than in the properties window).

Also, once one of these controls is on your form, right-click and then "Edit" it to change the caption. A mini-word-processor will pop up.

Enjoy,
Elroy

p.s. A reference to the Rich Text Box control should be made before you throw these into your project.
Attached Files

Vb6 - simple tcp connect

$
0
0
During the process of trying to figure out NAT Traversal (how to get around the problem of NAT blocking external connections), I had to simplify the process of TCP/IP connections. NewSocket was just too complex to start experimenting with it.

PrjTest3.vbp is a very simple example of connecting to a listening socket, and may help some users to understand how the Socket API (ws2_32.dll) functions in Vista or better operating systems. It does not contain a lot of error handling, it does not work with UDP, it does not work with IPv6, and it does not receive messages.

In the Form_Load event, the Winsock service is started (WSAStartup), and the destination IP Address & Port are defined. In cmdConnect_Click, an IPv4 TCP socket is created using a Socket call (aliased API_Socket). Then the local Socket Address structure (sa_local) is populated. This is where Version 2 of the Socket API differs substantially from Version 1. When using GetAddrInfo, binding to a particular socket is not required. We simply use address 0.0.0.0 and port 0, and GetAddrInfo will consult the local DNS to get the Server information and bind to the socket using the appropriate local interface and the first available local port. Because we are using an IP address instead of a domain name, that trip to the local DNS is not necessary. There will only be one address in the linked list, and we copy that information to the Hints structure. From there, we copy the socket portion to sa_dest. Now we have all the information necessary to send a Connection Request (SYN) to the destination. The destination should respond with a SYN-ACK, and the local socket should send an ACK (this is all handled by the API). Once connected, we send a simple text message.

That is about as far as we can go without implementing a callback procedure to intercept messages from the operating system. NewSocket uses Emiliano Scavuzzo's subclassing technique, which does not cause the IDE to crash and is able to differentiate the individual system messages from each socket. To put a socket into the listening mode is similar, but a little more complex. We have to create a socket, bind it to a user defined listening port, and put the socket into the listening mode with API_Listen. When a ConnectionRequest is received from the other end, the socket is closed, the connection is accepted on a different socket, and the socket once again is placed in the listening mode. This allows the server to accept multiple connections on the same port number. There is one caveat here though. Servers normally use blocking calls (each connection is on a separate thread) to handle large numbers of connections. However, we are using non-blocking calls, and the error WSAEWOULDBLOCK is not uncommon and should be ignored.

J.A. Coutts
Attached Files

[VB6] - Trick Advanced Tools.

$
0
0
Hello everyone!
I present to you a small project - Add-in that allows to some extent alleviate debugging some programs as well expand compilation possibilities. All the source codes are in the attachment.
This Add-in has the following features:
  1. Fixes the bug with Not Not Array statement that causes error "Expression too complex" if you'll work with float numbers;
  2. Allows to utilize the automatic conditional constants depending on run mode (IDE/EXE) look like in C++ (NDEBUG);
  3. Allows to disable integer overflow checking in IDE;
  4. Allows to disable floating point result checking in IDE;
  5. Allows to disable array bounds checking in IDE;
  6. Provides the compilation/linking events (both in IDE and EXE), i.e. you can run the commands before/after this events. By using this events you can do many useful things (encryption, replace OBJ files, static linking, etc.)


How does it work?



For fixing Not Not bug and disabling checking it uses the module of replacing of the opcodes handlers (P_Code) to ours. Firstly it finds the table of the opcodes by the signature in the ENGINE section of VBA6.dll module. There are two opcodes types - single-byte and double-bytes. Teh single-byte opcodes is less that 0xFB. It uses the length dissasembler by Ms-Rem that i ported to VB6. Besides it finds the subroutine that redirectes performing to the next opcode as well the subroutine that handles the errors. Since now it is very easy to get an access violation error i kept some checking. For example, access to uninitialized array causes the memory violation error - it handles that error. Because of there isn't an official documentation about VB6 opcodes (i've not found it) i did all the investigations, therefore some opcodes can raise error. In this case you can write them - i'll add handlers.
For others features it uses splicing of the following functions:
  1. TipCompileProject;
  2. TipCompileProjectFull;
  3. TipMakeExe2;
  4. TipFinishExe2.
TipSetConstantValues/TipGetConstantValues functions are used in order to set/get the conditional compilation arguments. The events is just calling of ShellExecuteEx function. There are events before/after project compilation (IDE/EXE) and linking. This project was weakly testing therefore it can contain bugs.
Regading,
Кривоус Анатолий (The trick)
Attached Files

[VB6] Using IAutoComplete / IAutoComplete2 including autocomplete with custom lists

$
0
0
IAutoComplete / IAutoComplete2 / IEnumString

SHAutocomplete has many well known limitations, the biggest being if you want to supply your own list to use with it. I was very impressed with Krool's work on this interface, and not wanting to include a whole other TLB set out to do it with oleexp.

Turns out it's far easier to work with using oleexp; the only major limitation being how to go about handling multiple autocompletes with different custom lists. UPDATE: Previously this class couldn't support multiple custom lists for different controls because the v-table swapping method was only passing IEnumString, rather than a full cEnumString class. If it were possible to get the full class, one might expect to be able to just change it to As cEnumString - but that didn't work. However changing it to a Long to get the pointer itself actually produced a pointer to the full instance of the class, and voilà, the undocumented-but-ever-useful vbaObjSetAddRef to the rescue, a reference to the class instance is born!
Code:

'Before:
'Public Function EnumStringNext(ByVal this As oleexpimp.IEnumString, ByVal celt As Long, ByVal rgelt As Long, ByVal pceltFetched As Long) As Long
'now:
Public Function EnumStringNext(ByVal this As Long, ByVal celt As Long, ByVal rgelt As Long, ByVal pceltFetched As Long) As Long
Dim cObj As cEnumString
vbaObjSetAddRef cObj, this
If (cObj Is Nothing) = False Then
    EnumStringNext = cObj.IES_Next(celt, rgelt, pceltFetched)
Else
    Debug.Print "esn obj fail"
End If

End Function

Finally, IAutoCompleteDropdown is used to provide the status of the dropdown autosuggest list. The .DropdownStatus method reports whether it's down, and the text of an item if an item in the list is selected. In the sample project, this is run on an automatically updated timer enabled in the 'basic filesystem' routine. It also exposes the .ResetEnumerator call to update the dropdown list while it's open.

Here's what the code looks like:

cAutoComplete.cls
Code:

Option Explicit

Private pACO As AutoComplete
Private pACL As ACListISF
Private pACL2 As IACList2
Private pACLH As ACLHistory
Private pACLMRU As ACLMRU
Private pACM As ACLMulti
Private pObjMgr As IObjMgr
Private pDD As IAutoCompleteDropDown
Private pUnk As oleexp3.IUnknown
Private m_hWnd As Long
Private pCust As cEnumString

Private Declare Function SysReAllocString Lib "oleaut32.dll" (ByVal pBSTR As Long, Optional ByVal pszStrPtr As Long) As Long
Private Declare Sub CoTaskMemFree Lib "ole32.dll" (ByVal PV As Long)

Private Sub Class_Initialize()
Set pACO = New AutoComplete
End Sub

Public Sub AC_Filesys(hWnd As Long, lOpt As AUTOCOMPLETEOPTIONS)
Set pACL = New ACListISF
pACO.Init hWnd, pACL, "", ""
pACO.SetOptions lOpt
pACO.Enable 1
m_hWnd = hWnd
End Sub
Public Sub AC_Disable()
pACO.Enable 0
End Sub
Public Sub AC_Enable()
pACO.Enable 1
End Sub
Public Sub AC_Custom(hWnd As Long, sTerms() As String, lOpt As AUTOCOMPLETEOPTIONS)
Set pCust = New cEnumString
pCust.SetACStringList sTerms
pACO.Init hWnd, pCust, "", ""
pACO.SetOptions lOpt
pACO.Enable 1
m_hWnd = hWnd
End Sub
Public Sub AC_ACList2(hWnd As Long, lOpt As AUTOCOMPLETEOPTIONS, lOpt2 As AUTOCOMPLETELISTOPTIONS)
Set pACL = New ACListISF
Set pACL2 = pACL
If (pACL2 Is Nothing) = False Then
    pACL2.SetOptions lOpt2
    pACO.Init hWnd, pACL2, "", ""
    pACO.SetOptions lOpt
    pACO.Enable 1
    m_hWnd = hWnd
Else
    Debug.Print "Failed to create IACList2"
End If
End Sub
Public Sub AC_History(hWnd As Long, lOpt As AUTOCOMPLETEOPTIONS)
Set pACLH = New ACLHistory
pACO.Init hWnd, pACLH, "", ""
pACO.SetOptions lOpt
pACO.Enable 1
m_hWnd = hWnd

End Sub
Public Sub AC_MRU(hWnd As Long, lOpt As AUTOCOMPLETEOPTIONS)
Set pACLMRU = New ACLMRU
pACO.Init hWnd, pACLMRU, "", ""
pACO.SetOptions lOpt
pACO.Enable 1
m_hWnd = hWnd

End Sub

Public Sub AC_Multi(hWnd As Long, lOpt As AUTOCOMPLETEOPTIONS, lFSOpts As AUTOCOMPLETELISTOPTIONS, bFileSys As Boolean, bHistory As Boolean, bMRU As Boolean, bCustom As Boolean, Optional vStringArrayForCustom As Variant)

  On Error GoTo e0

Set pACM = New ACLMulti
Set pObjMgr = pACM

If bFileSys Then
    Set pACL = New ACListISF
    Set pACL2 = pACL
    pACL2.SetOptions lFSOpts
    pObjMgr.Append pACL2
End If
If bMRU Then
    Set pACLMRU = New ACLMRU
    pObjMgr.Append pACLMRU
End If
If bHistory Then
    Set pACLH = New ACLHistory
    pObjMgr.Append pACLH
End If
If bCustom Then
    Dim i As Long
    Dim sTerms() As String
    ReDim sTerms(UBound(vStringArrayForCustom))
    For i = 0 To UBound(vStringArrayForCustom)
        sTerms(i) = vStringArrayForCustom(i)
    Next i
    Set pCust = New cEnumString
    pCust.SetACStringList sTerms
    pObjMgr.Append pCust
End If

pACO.Init hWnd, pObjMgr, "", ""
pACO.SetOptions lOpt
pACO.Enable 1
m_hWnd = hWnd
  On Error GoTo 0
  Exit Sub

e0:

    Debug.Print "cAutocomplete.AC_Multi.Error->" & Err.Description & " (" & Err.Number & ")"

End Sub

Public Function DropdownStatus(lpStatus As Long, sText As String)
If pDD Is Nothing Then
    Set pDD = pACO
End If
Dim lp As Long

pDD.GetDropDownStatus lpStatus, lp
SysReAllocString VarPtr(sText), lp
CoTaskMemFree lp

End Function
Public Sub ResetEnum()
If pDD Is Nothing Then
    Set pDD = pACO
End If
pDD.ResetEnumerator
End Sub

Implementing IEnumString's functions:
Code:

Public Function IES_Next(ByVal celt As Long, ByVal rgelt As Long, ByVal pceltFetched As Long) As Long
Dim lpString As Long
Dim i As Long
Dim celtFetched As Long
If rgelt = 0 Then
    IES_Next = E_POINTER
    Exit Function
End If

For i = 0 To (celt - 1)
    If nCur = nItems Then Exit For
    lpString = CoTaskMemAlloc(LenB(sItems(nCur)) & vbNullChar)
    If lpString = 0 Then IES_Next = S_FALSE: Exit Function
   
    CopyMemory ByVal lpString, ByVal StrPtr(sItems(nCur)), LenB(sItems(nCur) & vbNullChar)
    CopyMemory ByVal UnsignedAdd(rgelt, i * 4), lpString, 4&
   
    nCur = nCur + 1
    celtFetched = celtFetched + 1
Next i
 If pceltFetched Then
    CopyMemory ByVal pceltFetched, celtFetched, 4&
 End If
 If i <> celt Then IES_Next = S_FALSE

End Function
Public Function IES_Skip(ByVal celt As Long) As Long
If nCur + celt <= nItems Then
    nCur = nCur + celt
    IES_Skip = S_OK
Else
    IES_Skip = S_FALSE
End If
End Function

For the complete code, see the attached project.

Requirements
-oleexpimp.tlb v2.0 - I've forked and continued olelib2.tlb much the same as I did with the original. This new file replaces olelib2 in the same way oleexp3 replaces olelib (you can run search and replace). This file is included in the main oleexp download.
-oleexp3.tlb v3.8 - New version released with this project (29 Sep 2016)

Thanks
Krool's project mentioned above is what inspired me to do this, and I borrowed a few techniques from his project, especially for IEnumString.
Attached Files

Improved circle drawing

$
0
0
Here's my code for drawing a circle, that has significant improvements over the internal VB6 circle drawing commands.
Code:

Private Sub DrawCircle(ByVal X0 As Long, ByVal Y0 As Long, ByVal Radius As Long, ByVal Color As Long)
    Dim xMax As Long
    Dim Y As Long
    Dim X As Long
    For Y = -Radius To Radius
        xMax = Int(Sqr(Radius * Radius - Y * Y))
        For X = -xMax To xMax
            PSet (X0 + X, Y0 + Y), Color
        Next X
    Next Y
End Sub

The built in DrawWidth property makes a PSet dot bigger, so you can try to draw a circle with it, but it is not even close to being a perfectly symmetrical circle, until it reaches quite large sizes.

The builtin Circle command allows you to make a perfect circle, but the color you set using the color number in the Circle command only effects the circle's outline. To set the interior color of the circle, you have to set the FillColor property in a separate command, and furthermore you need to set the FillStyle to even make the interior of the circle visible (otherwise it's invisible/transparent). So you need to set 2 properties before even running the Circle command, and every time you want to change the color, you need to change the FillColor property.

This DrawCircle method that I created though, makes drawing a perfectly symmetric circle as easy as running one line of code, the code to call the method. All 4 parameters needed to draw the circle are specified at the time of calling the method.

The below sample code shows how to use this method in a MouseDown event in Form1, to make it draw a green circle of radius 10 (center pixel plus 10 pixels out from the center, which some people might call radius 11). The center of the circle will be wherever you click the mouse. Note that ScaleMode property of Form1 should be Pixel (not the default Twip), and that AutoRedraw should be set to True.
Code:

Private Sub Form_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single)
    DrawCircle X, Y, 10, &HFF00&
End Sub

Using OERN (On Error Resume Next)

$
0
0
This isn't any substantial piece of code, but it points out a potential problem I see experienced programmers making on these forums. Also, I must give Bonnie West some credit for pointing this out to me and forcing me to develop a clear understanding of it all.

Let me start by outlining how the three On Error... statements work, which isn't well documented in the MSDN.

On Error Resume Next - Always clears the ERR object upon execution, but leaves results in the ERR object even after end/exit from a procedure call.

On Error Goto LineLabel - Always clears the ERR object upon execution, and the ERR object is also cleared upon end/exit from a procedure call, regardless of whether Resume, Resume LineLabel, or Resume Next is used.

On Error Goto 0 - Always clears the ERR object upon execution.

It's the difference between On Error Resume Next and On Error Goto LineLabel that is often unappreciated. To illustrate, I've set up the following example. Just paste it into Form1's code and execute:

Code:


Option Explicit

Private Sub Form_Load()
    MsgBox SomeTestWithOern_TrueIfError
    Unload Me
End Sub

Private Function SomeTestWithOern_TrueIfError() As Boolean
    Dim i As Long
    Dim b As Boolean
    '
    On Error Resume Next
    i = 1 / 1 ' Does NOT cause error.
    '
    ' Just some other function maybe used herein.
    ' In this example, nothing is done with the return, but it could be.
    b = AnotherTestWithOren_TrueIfError
    '
    ' And now we return, thinking that we've only tested our i = 1/1 line for an error.
    SomeTestWithOern_TrueIfError = Err.Number <> 0
End Function

Private Function AnotherTestWithOren_TrueIfError() As Boolean
    Dim i As Long
    '
    On Error Resume Next
    i = 1 / 0 ' Causes error.
    AnotherTestWithOren_TrueIfError = Err.Number <> 0
End Function

In case you don't want to execute it, the message box reports "True", which is misleading. The SomeTestWithOern_TrueIfError didn't technically have any error. That's the point I'm trying to make.

And now, this can be fixed with the addition of a single line, an "On Error Goto 0" at the end of AnotherTestWithOren_TrueIfErro, as follows:

Code:


Option Explicit

Private Sub Form_Load()
    MsgBox SomeTestWithOern_TrueIfError
    Unload Me
End Sub

Private Function SomeTestWithOern_TrueIfError() As Boolean
    Dim i As Long
    Dim b As Boolean
    '
    On Error Resume Next
        i = 1 / 1 ' Does NOT cause error.
        '
        ' Just some other function maybe used herein.
        ' In this example, nothing is done with the return, but it could be.
        b = AnotherTestWithOren_TrueIfError
        '
        ' And now we return, thinking that we've only tested our i = 1/1 line for an error.
        SomeTestWithOern_TrueIfError = Err.Number <> 0
    On Error GoTo 0
End Function

Private Function AnotherTestWithOren_TrueIfError() As Boolean
    Dim i As Long
    '
    On Error Resume Next
        i = 1 / 0 ' Causes error.
        AnotherTestWithOren_TrueIfError = Err.Number <> 0
    On Error GoTo 0
End Function

This time, the message box reports "False"!!! In fact, I put the "On Error Goto 0" in both test functions, just as good programming practice. Also, just to make sure I always "turn off" my "On Error Resume Next" statements, I've adopted the convention of indenting between them.

Regards,
Elroy

EDIT1: Just as an FYI, even my recommendation isn't a perfect fix because the ERR object is truly a global object. Clearing ERR anywhere clears it everywhere.

[CODE] Responsive applications in VB6 (and immune to resolution changes)

$
0
0
Make VB6 applications look, feel and work state-of-the-art.

See this video for more:
https://youtu.be/2RPnJotSYj0

I HAVE HEAVILY MODIFIED THE CODE NOT COMPLETELY CODED IT. THE CREDITS CAN BE FOUND BELOW. I HAVE ALSO CONSOLIDATED OTHER PROJECTS TO CREATE A BASIC PACKAGE FOR BEGINNERS THAT WILL MAKE YOUR APPLICATIONS LOOK GREAT.

With the following code, your form controls will automatically resize with the forms and look awesome (The looks have to do with manifests which I have credited below). Also, it is responsive to resolution changes.

To use the source code:
Visit the manifest creator page for instructions on theming.

For the automatic resizing:
Add the module ScalingModule (Module2) to your project.

Then declare in each form:
Private InitialControlList() As ControlInitial

Then insert the following code in all forms to be resized:

Private Sub Form_Load()
InitialControlList = GetLocation(Me)
ReSizePosForm Me, Me.height, Me.width, Me.Left, Me.Top
End Sub

Private Sub Form_Resize()
ResizeControls Me, InitialControlList
End Sub

Play around with the sample project for more! You may need to add a few components/references for it to work. The exe should probably work out of the box (But I can't assure it).

Credits:

I modified (heavily) the inefficient code for resizing form and controls found here:
http://www.dreamincode.net/forums/to...reen-size-vb6/

The manifest creator for better looks:
http://www.vbforums.com/showthread.p...nifest-Creator
(See #79 on page 2 for better code)

A TON of interfaces to make VB6 look better:
http://www.vbforums.com/showthread.p...ary-oleexp-tlb

Taskbar Progressbar Animation:
http://www.vbforums.com/showthread.p...n-taskbar-etc)

Source Codes:

The Project's Source code (all that you saw in the video) can be found here:
http://bit.do/vb6-1-all

Everything except the taskbar integration:
http://bit.do/vb6-1-no-task

Please credit me and the others above!

VB6 Build-in types extension library (FTypes)

$
0
0
This project aim is to extend Visual Basic 6.0 build-in types (like Integer, Long, String and etc.) in order to make work with it more convinient ("one-liner" style if needed) and support extended properties/methods on that basic types.

Classes:

- ArrayEx
- ByteEx
- IntegerEx
- LongEx
- DoubleEx
- StringEx

Sample usage:

Dim s As New StringEx

s = "Hello"

MsgBox s.Clone.Parse(" Hello VBForum ").TrimL.Insert(0, "'").Concat("!!!'").Upper 'produces 'HELLO VBFORUM !!!'

MsgBox s 'produces Hello since Clone was used in first msgbox


Notes:

- Each class has default property Value that is used to assign/read value of appropriate basic VB 6.0 type;
- Each class has Clone method that produces a new instance of class with same value;
- If class function returns same type as class has - that means NO new instanse created (except Clone method) and call modifies initial value assigned at first use of class Value property;

Most valuable features:

- Aim on performance;
- ArrayEx class reports of dimensions, elements size and allows to get pointer of any type of basic array assigned via Value property;
- StringEx class has powerfull Parse method that can get byte arrays and produce utf-16 native VB6 string (f.e. from ansi, utf-8 with/without bom, utf-16 with/without bom/LE/BE text file);
- StringEx class works with dynamically buffered string, so "Concat" and other methods are very usefull within loops (f.e. 'This Is My New Test String' concatenation of 50000 iterations took ~0.37 sec against same VB6 concatenation taking ~23 sec);
- StringEx class has additional methods like Duplicate, Insert & Remove and etc as well as native to a developer Trim, TrimL/R, Replace, Left, Right and etc methods;
- StringEx class exposes a string pointer so you can manipulate it with your own RtlMoveMemory-based routines;
- ByteEx/IntegerEx/LongEx/DoubleEx classes each have Parse method capable to extract numbers from Variant string as well as get value from numeric types directly without overflow;

List of revisions:

29-Oct-2016
- 1.0.0 (with updates)
Attached Files

VB6 Built-in types extension library (FTypes)

$
0
0
This project aim is to extend Visual Basic 6.0 built-in types (like Integer, Long, String and etc.) in order to make work with it more convinient ("one-liner" style if needed) and support extended properties/methods on that basic types.

Classes:

- ArrayEx
- ByteEx
- IntegerEx
- LongEx
- DoubleEx
- StringEx

Sample usage:

Dim s As New StringEx

s = "Hello"

MsgBox s.Clone.Parse(" Hello VBForum ").TrimL.Insert(0, "'").Concat("!!!'").Upper 'produces 'HELLO VBFORUM !!!'

MsgBox s 'produces Hello since Clone was used in first msgbox


Notes:

- Each class has default property Value that is used to assign/read value of appropriate basic VB 6.0 type;
- Each class has Clone method that produces a new instance of class with same value;
- If class function returns same type as class has - that means NO new instanse created (except Clone method) and call modifies initial value assigned at first use of class Value property;

Most valuable features:

- Aim on performance;
- ArrayEx class reports of dimensions, elements size and allows to get pointer of any type of basic array assigned via Value property;
- StringEx class has powerfull Parse method that can get byte arrays and produce utf-16 native VB6 string (f.e. from ansi, utf-8 with/without bom, utf-16 with/without bom/LE/BE text file);
- StringEx class works with dynamically buffered string, so "Concat" and other methods are very usefull within loops (f.e. 'This Is My New Test String' concatenation of 50000 iterations took ~0.37 sec against same VB6 concatenation taking ~23 sec);
- StringEx class has additional methods like Duplicate, Insert & Remove and etc as well as native to a developer Trim, TrimL/R, Replace, Left, Right and etc methods;
- StringEx class exposes a string pointer so you can manipulate it with your own RtlMoveMemory-based routines;
- ByteEx/IntegerEx/LongEx/DoubleEx classes each have Parse method capable to extract numbers from Variant string as well as get value from numeric types directly without overflow;

List of revisions:

29-Oct-2016
- 1.0.0 (with updates)
Attached Files

VB6 - Simple Sock

$
0
0
SimpleSock basically performs the same functions as NewSocket. Like NewSocket, it supports IPv6 as well as IPv4. This more or less restricts it's use to Windows Vista or better, as older operating systems do not support dual stack using "ws2_32.dll". Unlike NewSocket, it cannot be used as a Control Array because of the way it handles listening sockets (more on that later).

While Emiliano Scavuzzo's subclassing technique remains fairly much intact, the rest of the program has been completely rewritten and hopefully simplified. Notifying the Class with the protocol being used (TCP/UDP) is no longer required. Instead there are separate routines to handle each task. Lets take a look at some of the basics.

UDP (User Datagram Protocol)
I started with this one because it is the simplest. UDP is a peer-to-peer protocol, because both parties are equal and either one can initiate the conversation. It is also connectionless. That is to say that data is just sent with no idea if it made it correctly to the other end. The packet size is also very limited (256 bytes). For these reasons, it is rarely used for sensitive bulk data. In the sample program provided, an instance of SimpleSock is created called "mSocket". "mSocket" defaults to IPv4, so if IPv6 is required, you must notify the instance by setting the mSocket.IPvFlg to 6. To initiate a UDP session, you simply call:
Code:

mSocket.UDPInit(Destination, PortConnect, PortLocal)
The Destination Port and the Local Port are required, but if it is not known, the Destination can be left blank. This might be the case if the initial receiver does not know where the first message will be originating from. If blank, the GetAddrInfo function will return the LoopBack address (127.0.0.1 for IPv4 & ::1 for IPv6). You can test this functionality by setting the UDP option and the Local and Destination ports (they can both be the same), and typing a message in the text box followed by an <Enter>. The program will send the message to itself and the sender address (127.0.0.1/::1) will appear in the Destination text box. In the real world however, the sender's IP address will appear in the Destination text box, at which point the user can once again call the UDPInit function to update its information.

So what information gets updated? The first time through, UPDInit creates the socket and binds it to the Local Port. It then creates a "sockaddr" for the destination using GetAddrInfo. The sockaddr structure is the part that gets updated. For those familiar with the original IPv4 structure, it looked like this:
Code:

Private Type sockaddr_in
    sin_family          As Integer  '2 bytes
    sin_port            As Integer  '2 bytes
    sin_addr            As in_addr  '4 bytes
    sin_zero(0 To 7)    As Byte    '8 bytes
End Type                            'Total 16 bytes
or reflected as:
Private Type sockaddr
    sa_family          As Integer  '2 bytes
    sa_data(0 to 13)    As Byte    '14 bytes
End Type                            'Total 16 bytes

When IPv6 came along, this had to be changed to:
Code:

Private Type sockaddr_in6
    sin6_family        As Integer  '2 bytes
    sin6_port          As Integer  '2 bytes
    sin6_flowinfo      As Long    '4 bytes
    sin6_addr          As in6_addr '16 bytes
    sin6_scope_id      As Long    '4 bytes
End Type                            'Total 28 bytes
Private Type sockaddr
    sa_family          As Integer  '2 bytes
    sa_data(0 to 25)    As Byte    '26 bytes
End Type                            'Total 28 bytes

The larger sockaddr is used to carry the information for both IP protocols, with the extra 12 bytes being ignored for IPv4. Because the packet data is of limited length, UDP data is left in the Winsock Buffer and the calling program is informed of it's length. The calling program then recovers the data and empties the Winsock Buffer.

To send data via UDP, we need the Socket Handle, the binary Data and it's length, and the sockaddr and it's length for the destination. The data is passed to the output buffer as string data and converted to byte data, or sent directly to the output buffer as byte data. Providing that the sockaddr has been updated correctly, all the information is available to send back to the other end with a call to mSocket.UDPSend.

TCP (Transport Control Protocol)
The more commonly used protocol is TCP. There are actually 2 types of TCP, because one end acts as the server, and one end acts as the client. Lets look at the client end first, because it is the simpler. We establish a connection with the other end by calling:
Code:

mSocket.TCPConnect(Destination, PortConnect)
We supply the Destination as either an IP address or a domain name, and the destination port as a long variable. GetAddrInfo will find the IP address for a Domain name, provided the name is defined in a DNS host, or it is a local network name. Normally, the Local port is not required, as the API will find the first available port. SimpleSock however does have the ability to use a selected port. If the port selected is not being used, it will bind the created socket to the port. It also eliminates the TIME_WAIT period by setting the options "SO_LINGER" & "SO_REUSEADDR". For reasons unknown, I had to set both these options to achieve the desired result. The API will send out a SYN request to the other end, and wait for a response. If the other end is listening for a connection request, it will send a SYN_ACK back to us. The API will acknowledge this by sending an ACK, and the connection is established. Once the connection is established, a "Connect" event is fired back to the calling program, and data can be sent immediately using "TCPSend".

Receipt of data is similar to UDP, except that SimpleSock removes the data from the Winsock buffer and adds it to it's own buffer. This is necessary because sent records can be quite lengthy, and are received in chunks. What is different about SimpleSock is the provision to handle encrypted data. This is accomplished by using 2 separate event messages (DataArrival/EncrDataArrival) to inform the calling program of data arrival.

To act as a TCP server, the socket is created and bound to the selected port using:
Code:

mSocket.Listen(PortListen)
When a connection request is received from the other end, the API sends an "FD_ACCEPT" message to the "PostSocket" routine. This is where SimpleSock differs from NewSocket and it predecessors. The older programs would create a new socket and a temporary instance of the class to handle it. It would then be registered as an "Accept" item, before firing off a "ConnectionRequest" event to the calling program. The calling program would then close the Listening socket and call the class "Accept" function with the new socket handle. Closing of the listening socket and de-registering it caused the Socket Collection to be destroyed and the Window closed. The new socket would then be registered as a normal socket (causing a new Window and Socket Collection to be created), ownership of the new socket transferred from the temporary Class to the original Class, and the temporary Class destroyed. The calling program would then create a new Listening Socket. If this all sounds very complicated, it was. But it was necessary in order to duplicate the way that the MS Winsock Control handled things when used as a Control Array.

When SimpleSock receives an "FD_ACCEPT" message from an incoming connection attempt, it creates and registers the new socket as it normally would, and leaves the original listening socket intact. It then fires off a "ConnectionRequest" event to the calling program. The calling program then calls mSocket.Accept with the new socket handle. The Accept function saves the listening socket handle, sets a flag, and readies the new socket to receive and send data. If another connection request is received while the new socket is open, it will be ignored because the new socket is not in the listening mode. When the new socket is closed, the listening socket handle will be restored, and another connection request will be entertained.

This simplified approach is only useful when using the SimpleSock Class directly. It will not be effective if it was made into a Control and used as a Control Array. The next step is to make the Class able to handle multiple connections on the same listening port without creating a Control.

J.A. Coutts

Note: When using Link Local IPv6 addresses to communicate with older systems such as Vista, you may have to add the interface (eg. %8) to the IP address.

Note: The sample program is a demonstration program that uses various aspects of the socket function. It may not work properly when switching from one to another. Restart the program to test different functions.
Attached Images
 
Attached Files

[VB6] SAX: Not just for XML

$
0
0
MXHTMLWriter is a handy feature added to MSXML SAX2 in version 6.0, but few have probably heard of SAX and few still of MXHTMLWriter.

See MXHTMLWriter CoClass for an overview.

There are several ways to use MXHTMLWriter but here I'll turn it "inside out" by explicitly raising events to it via IVBSAXContentHandler instead of letting other parts of MSXML raise the events. This is a very basic example showing how to do that to write HTML, and in this case the demo involves simple reporting.

Depending on your purpose you might want the results in different ways. Here I show how to get file output, String output, and Byte array output (since for that we can get UTF-8 or other character encodings).

It should be plenty speedy enough for most purposes:

Name:  sshot.png
Views: 56
Size:  3.1 KB

Here is the crux of the demo:

Code:

Private Sub Report(ByRef Dest As Variant, Optional ByVal Encoding As String = "ASCII")
    'Dest:    Can be an instance of an IStream implementation or a String.
    '
    'Encoding: Can be "UTF-8" or "Windows-1252" or "UTF-16" etc. as desired.
    '          Always ignored for String output which is always UTF-16
    '          ("Unicode").
    Const REPORT_TITLE As String = "January 2009 Sales"
    Const CSS_STYLES As String = vbNewLine _
        & "*{font:normal normal normal 8pt Arial;}" & vbNewLine _
        & "th,td{border:1px solid black;}" & vbNewLine _
        & "th{background-color:royalblue;color:white;font-weight:bold;}" & vbNewLine _
        & "td{background-color:white;color:green;}" & vbNewLine _
        & "table,th,td{border-collapse:collapse;}" & vbNewLine _
        & ".SH{color:red;}"
    Dim Attrs As MSXML2.SAXAttributes60
    Dim Handler As MSXML2.IVBSAXContentHandler
    Dim Writer As MSXML2.MXHTMLWriter60
    Dim FieldsUB As Long
    Dim Fields() As ADODB.Field
    Dim Col As Long
    Dim Row As Long
    Dim LatitudeField As Long
    Dim Value As Variant

    Set Attrs = New MSXML2.SAXAttributes60
    Set Writer = New MSXML2.MXHTMLWriter60
    Set Handler = Writer
    With Writer
        .disableOutputEscaping = False
        .indent = True
        .Encoding = "ASCII"
        .byteOrderMark = True 'Has no effect for 8-bit encodings or any String output.
        .output = Dest 'Can be an IStream implementation, or a String value to set
                      'the output type to String.
    End With
    With RS
        .MoveFirst
        FieldsUB = .Fields.Count - 1
        ReDim Fields(FieldsUB)
        For Col = 0 To FieldsUB
            Set Fields(Col) = .Fields(Col)
            If Fields(Col).Name = "Latitude" Then LatitudeField = Col
        Next
    End With
    With Handler
        .startDocument
        .startElement "", "", "HTML", Attrs
        .startElement "", "", "HEAD", Attrs 'Auto-emits a META tag for encoding.
        Attrs.addAttribute "", "", "name", "", "generator"
        Attrs.addAttribute "", "", "content", "", App.CompanyName _
                                                & " " & App.EXEName _
                                                & " " & CStr(App.Major) _
                                                & "." & CStr(App.Minor)
        .startElement "", "", "META", Attrs
        Attrs.Clear
        .endElement "", "", "META"
        .startElement "", "", "TITLE", Attrs
        .characters REPORT_TITLE
        .endElement "", "", "TITLE"
        Attrs.addAttribute "", "", "type", "", "text/css"
        .startElement "", "", "STYLE", Attrs
        Attrs.Clear
        .characters CSS_STYLES
        .endElement "", "", "STYLE"
        .endElement "", "", "HEAD"
        .startElement "", "", "BODY", Attrs
        .startElement "", "", "TABLE", Attrs
        .startElement "", "", "TR", Attrs
        For Col = 0 To FieldsUB
            .startElement "", "", "TH", Attrs
            .characters Replace$(Fields(Col).Name, "_", " ")
            .endElement "", "", "TH"
        Next
        .endElement "", "", "TR"
        Do Until RS.EOF
            'Hightlight rows for Southern Hemisphere:
            If Fields(LatitudeField).Value < 0 Then
                Attrs.addAttribute "", "", "class", "", "SH"
            Else
                Attrs.Clear
            End If
            .startElement "", "", "TR", Attrs
                For Col = 0 To FieldsUB
                    .startElement "", "", "TD", Attrs
                    Value = Fields(Col).Value
                    If Not IsNull(Value) Then .characters CStr(Value)
                    .endElement "", "", "TD"
                Next
            .endElement "", "", "TR"
            RS.MoveNext
        Loop
        .endElement "", "", "TABLE"
        .endElement "", "", "BODY"
        .endElement "", "", "HTML"
        .endDocument
    End With
    With Writer
        .Flush
        If VarType(Dest) = vbString Then
            Dest = .output 'Fetch String output.
        End If
    End With
End Sub

The attachment contains some raw data, which is why it is so large.

MSXML 6.0 has been part of Windows since Vista. You might still be able to download a redist version for XP SP2 or maybe SP3 from Microsoft.
Attached Images
 
Attached Files

[VB6, Vista+] Remember Open/Save state per-dialog instead of per-app (IFileDialog)

$
0
0
This code tip applies to the new Common Item Dialog interfaces, IFileOpenDialog and IFileSaveDialog, that replace the old Common Dialog control and GetOpenFileName/GetSaveFileName API calls in Windows Vista and newer. See this project for an introduction to using these interfaces.

Users definitely appreciate the common dialogs opening to the last path, and programmers that this is automatically handled by Windows. Previously, this was limited however to a single state memory for the entire app; but among the many new features of the new IFileDialog-based Common Dialogs is the ability to have Windows manage it automatically for individual dialogs, as well as clear the settings without mucking about in the registry.

The key is IFileDialog's .SetClientGuid method. You can specify a unique GUID for your dialog, and the settings like last path are stored under the GUID, instead of under your app's name.

This example code is based on accessing the new Common Item Dialogs through my oleexp type library, with the IID module loaded as well.

First, establish GUIDs for each dialog you want to have its own settings. Two are shown here, but there's no limit to how many an app can have. These must new, unique GUIDs. Visual Studio 6 came with a tool called GUIDGEN, but there's plenty of other GUID generators out there.

Code:

Private Declare Function CLSIDFromString Lib "ole32" (ByVal lpszGuid As Long, pGuid As Any) As Long
Private Const sGUID_Dialog1 = "{A4BF774D-0029-4c8f-9174-B397211B92F5}"
Private Const sGUID_Dialog2 = "{83E461A9-5341-46f5-8825-EAC176603E94}"

Private Function GUID_Dialog1() As UUID
Static iid As UUID
 If (iid.Data1 = 0) Then Call CLSIDFromString(StrPtr(sGUID_Dialog1), iid)
 GUID_Dialog1 = iid
End Function
Private Function GUID_Dialog2() As UUID
Static iid As UUID
 If (iid.Data1 = 0) Then Call CLSIDFromString(StrPtr(sGUID_Dialog2), iid)
 GUID_Dialog2 = iid
End Function

Then all you have to do it associate the Common Dialog with the GUID. Per MSDN, this should be the very first call after creating the dialog:
Code:

Dim pFOD As New FileOpenDialog
pFOD.SetClientGuid GUID_Dialog1 'whichever GUID you want here
'[...now set any other options

And that's all there is to it. If you want to clear the saved state, simply call .ClearClientData after setting the associated GUID (you can also clear it for the app-level state by calling it without having set a GUID).

Attached is a small project showing this technique in action. It requires oleexp.tlb (version 4.0 or newer is referenced) and addon mIID.bas from the oleexp zip.
Attached Files

[VB6, Vista+] Direct access to the system-wide image thumbnail cache

$
0
0

While in general you want to use IShellItemImageFactory to get these thumbnails, as that will also return icons, if you're interested in more control or better performance, you can use IThumbnailCache and the Windows supplied implementation LocalThumbnailCache for direct access to the main system thumbnail cache.

There's a large number of additional options, and even more still if you're using Windows 8 or higher. You can choose whether to extract if not in the cache, only retrieve it if cached already, or extract it again to update the cached version. While not shown in the picture above, these thumbnails do properly render transparency.

Main routine (see full project for declares, module-level vars, etc):
Code:

Private Sub Command1_Click()
Dim fod As New FileOpenDialog
Dim kfPics As IShellItem
Dim tSpec() As COMDLG_FILTERSPEC
Dim pBitmap As ISharedBitmap
Dim hBmp As Long
Dim lFlag As WTS_CACHEFLAGS
Dim btID As WTS_THUMBNAILID
Dim tSZ As SIZE
Dim lOpt As WTS_FLAGS
On Error GoTo e0

ReDim tSpec(1)

tSpec(0).pszName = "Image Files"
tSpec(0).pszSpec = "*.gif;*.jpg;*.png;*.ico;*.bmp"
tSpec(1).pszName = "All Files"
tSpec(1).pszSpec = "*.*"

fod.SetClientGuid GUID_ThisProject
fod.SetTitle "Choose an image"
fod.SetOkButtonLabel "Show Thumbnail"
fod.SetOptions FOS_DONTADDTORECENT
fod.SetDefaultFolder kfPics
fod.SetFileTypes 2&, VarPtr(tSpec(0).pszName)
fod.Show Me.hWnd

On Error Resume Next
fod.GetResult psiFile
On Error GoTo e0

If (psiFile Is Nothing) = False Then
    If (pCache Is Nothing) Then
        Set pCache = New LocalThumbnailCache
    End If
    'Note: Many WTS options are Win8+ only. Here we're only demonstrating basic ones that are Win7+
    If Option1(0).Value = True Then lOpt = WTS_EXTRACT Or WTS_SCALETOREQUESTEDSIZE
    If Option1(1).Value = True Then lOpt = WTS_INCACHEONLY Or WTS_SCALETOREQUESTEDSIZE
    If Option1(2).Value = True Then lOpt = WTS_FORCEEXTRACTION Or WTS_SCALETOREQUESTEDSIZE
    If Check1.Value = vbChecked Then
        If (Option1(0).Value = True) Or (Option1(2).Value = True) Then lOpt = lOpt Or WTS_EXTRACTDONOTCACHE
    End If
    pCache.GetThumbnail psiFile, cxThumb, lOpt, pBitmap, lFlag, btID
    If (pBitmap Is Nothing) = False Then
        pBitmap.GetSize tSZ
        Debug.Print "Got bitmap obj, cx=" & tSZ.CX & ",flag=0x" & Hex$(lFlag)
        PrintThumbID btID
        pBitmap.GetSharedBitmap hBmp
        Debug.Print "hBITMAP=" & hBmp
        Picture1.Cls
        hBitmapToPictureBox Picture1, hBmp
        pBitmap.Detach hBmp
        DeleteObject hBmp
    Else
        Debug.Print "Failed to get bitmap obj, flag=0x" & Hex$(lFlag)
    End If
Else
    Debug.Print "No file selected."
End If

Exit Sub

e0:
    Debug.Print "GetThumb.Error->" & Err.Description & " (0x" & Hex$(Err.Number) & ")"
End Sub

Requirements
-Windows Vista or higher. Some options in demo project are Windows 7 and higher. The interface itself has many options only available on Windows 8 and higher, although none are used in the demo.
-oleexp.tlb version 4.0 or higher. Only needed for the IDE, doesn't need to be redistributed with your exe.

Notes
Thumbnails are looked up by providing an IShellItem representing the file. In the sample, this is super easy as that's what's returned from the FileOpenDialog. But without that, you can get that reference from any number of methods, including SHGetItemFromParsingName:
Code:

Public Declare Function SHCreateItemFromParsingName Lib "shell32" (ByVal pszPath As Long, pbc As Any, riid As UUID, ppv As Any) As Long

Call SHCreateItemFromParsingName(StrPtr(pathtofile), ByVal 0&, IID_IShellItem, psi)

Or from a pidl,
Code:

Public Declare Function SHCreateItemFromIDList Lib "shell32" (ByVal pidl As Long, riid As UUID, ppv As Any) As Long
Alternative Access to Thumbnail
Closely related, there's a very simple way to get the thumbnail of an image file (and only image file; this won't return a normal icon either) represented by an IShellItem:

Code:

Dim hbmTP As Long
Dim pTP As IThumbnailProvider
Dim psiImg As IShellItem

Call SHCreateItemFromParsingName(StrPtr("C:\folder\MyImage.jpg"), ByVal 0&, IID_IShellItem, psiImg)
psiImg.BindToHandler 0&, BHID_ThumbnailHandler, IID_IThumbnailProvider, pTP
If (pTP Is Nothing) = False Then
    pTP.GetThumbnail 128&, hbmTP, WTSAT_ARGB 'where 128 is the desired size. 16-256, maybe 512 work the best
    Debug.Print "hbm=" & hbmTP
    hBitmapToPictureBox Picture1, hbmTP
Else
    Debug.Print "no ptp"
End If

Where the hBitmapToPictureBox is the same as the demo project. This code snippet also makes use of mIID.bas from the oleexp download.
This method has the bonus of an option controlling transparency.
Attached Files

[VB6, Vista+] Add the Windows Send To submenu to your popup menu

$
0
0

So at first I set out to just duplicate the functionality, but then immediately saw the FOLDERID_SendTo special folder, and realized that it should be possible to add a fully functional SendTo menu. It's not just creating something similar, it actually implements the same Send To menu you get in Explorer- using shell interfaces to perform the actions the exact same way.

This project is a little high on the complexity scale, but not too bad.

The core parts of the code look like this:
Code:

Public psiSTChild() As IShellItem 'need to store the loaded SendTo items so they can be called when selected
Public Const widBaseST = 2800&
Public widSTMax As Long

Public Function GenerateSendToMenu() As Long
'it's the callers responsibility to call DestroyMenu()
Dim mii As MENUITEMINFOW
Dim i As Long, j As Long, k As Long
Dim hIcon As Long
Dim isiif As IShellItemImageFactory
Dim hMenu As Long
Dim lpCap As Long
Dim sCap As String
hMenu = CreateMenu()
Dim s1 As String, lp1 As Long
Dim psiSendTo As IShellItem
Dim nChild As Long
Dim pcl As Long
Dim penum As IEnumShellItems

On Error GoTo e0

Call SHGetKnownFolderItem(FOLDERID_SendTo, KF_FLAG_DEFAULT, 0&, IID_IShellItem, psiSendTo)
If (psiSendTo Is Nothing) = False Then
    psiSendTo.BindToHandler 0&, BHID_EnumItems, IID_IEnumShellItems, penum
    If (penum Is Nothing) = False Then
        ReDim psiSTChild(0)
        Do While (penum.Next(1&, psiSTChild(nChild), pcl) = S_OK)
            psiSTChild(nChild).GetDisplayName SIGDN_NORMALDISPLAY, lpCap
            sCap = LPWSTRtoStr(lpCap)
            Set isiif = psiSTChild(nChild)
            isiif.GetImage 16, 16, SIIGBF_ICONONLY, hIcon
            With mii
                .cbSize = Len(mii)
                .fMask = MIIM_ID Or MIIM_STRING Or MIIM_BITMAP
                .wID = (widBaseST + j)
                .cch = Len(sCap)
                .dwTypeData = StrPtr(sCap)
                .hbmpItem = hIcon
                Call InsertMenuItemW(hMenu, j, True, mii)
   
                Call DestroyIcon(hIcon)
                j = j + 1
            End With
            Set isiif = Nothing
            nChild = nChild + 1
            ReDim Preserve psiSTChild(nChild)
        Loop
    Else
        Debug.Print "GenerateSendToMenu->Failed to get enum obj"
    End If
Else
    Debug.Print "GenerateSendToMenu->Failed to get SendTo folder obj"
End If
widSTMax = j
GenerateSendToMenu = hMenu
Exit Function
e0:
Debug.Print "GenerateSendToMenu.Error->" & Err.Description & " (" & Err.Number & ")"
End Function

GenerateSendToMenu creates a submenu for a standard API popup menu. The shell items loaded from the SendTo folder are stored in a public array, so we can access them after a selection is made:
Code:

If idCmd Then
    Select Case idCmd
        Case widBaseST To (widBaseST + widSTMax)
            Dim lp As Long
            psiSTChild(idCmd - widBaseST).GetDisplayName SIGDN_NORMALDISPLAY, lp
            If MsgBox("Send to " & LPWSTRtoStr(lp) & "?", vbYesNo, "Confirm SendTo") = vbYes Then
                ExecSendTo (idCmd - widBaseST)
            End If
    End Select
End If

Finally, we use a technique you may recall from my Create Zip Files demo- dropping an IDataObject representing the files we're moving onto an IDropTarget belonging to the destination:
Code:

Private Sub ExecSendTo(nIdx As Long)
Dim pdt As IDropTarget
psiSTChild(nIdx).BindToHandler 0&, BHID_SFUIObject, IID_IDropTarget, pdt
If ((pdt Is Nothing) = False) And ((pdoFiles Is Nothing) = False) Then
    Dim dwEffect As Long
    dwEffect = DROPEFFECT_COPY Or DROPEFFECT_MOVE Or DROPEFFECT_LINK
    pdt.DragEnter pdoFiles, MK_LBUTTON, 0&, 0&, dwEffect
    pdt.Drop pdoFiles, MK_LBUTTON, 0&, 0&, dwEffect
End If
End Sub

As an added bonus, picking the files with IFileOpenDialog makes it super-easy to get the IDataObject for the files, pdoFiles.
Code:

Dim fod As New FileOpenDialog
Dim psiaRes As IShellItemArray
With fod
    .SetOptions FOS_ALLOWMULTISELECT Or FOS_DONTADDTORECENT
    .SetTitle "Choose files for SendTo..."
    .Show Me.hWnd
    .GetResults psiaRes
    If (psiaRes Is Nothing) = False Then
        psiaRes.BindToHandler 0&, BHID_DataObject, IID_IDataObject, pdoFiles
    End If
End With

Requirements
-Windows Vista or newer
-oleexp.tlb v4.0 or higher (only for IDE, doesn't need to be included with compiled exe)
-mIID.bas - included in the oleexp download

Extra Thoughts
Generate IDataObject from file list
If you want to get an IDataObject but just have a list of file paths, you can do it like this, where sSelFullPath is a string array of full paths to the files:
Code:

Public Declare Function SHCreateShellItemArrayFromIDLists Lib "shell32" (ByVal cidl As Long, ByVal rgpidl As Long, ppsiItemArray As IShellItemArray) As Long
Public Declare Function ILCreateFromPathW Lib "shell32" (ByVal pwszPath As Long) As Long


Dim psia As IShellItemArray
Dim pdoFiles As oleexp.IDataObject
Dim apidl() As Long
Dim i As Long

ReDim apidl(0)
For i = 0 To UBound(sSelFullPath)
    ReDim Preserve apidl(i)
    apidl(i) = ILCreateFromPathW(StrPtr(sSelFullPath(i)))
Next i
Call SHCreateShellItemArrayFromIDLists(UBound(apidl) + 1, VarPtr(apidl(0)), psia)
psia.BindToHandler 0&, BHID_DataObject, IID_IDataObject, pdoFiles

Customizing the enumeration
Say, for example, you want to override the user preference for hidden files (in the pic up top, Desktop.ini is shown because my system is set to show all hidden/system files). There's two ways go about this. If you're targeting only Windows 8 and above, you can play around with the wonderful world of the IBindCtx parameter with STR_ENUM_ITEMS_FLAGS
Windows Vista and Windows 7 however, you're going to have to drop down to IShellFolder and use the .EnumObjects SHCONTF options. Doing it in VB with oleexp requires far less code than Raymond uses, if anyone is really interested I could write up the VB code.
Attached Files

[VB6] SHBrowseForFolder - Custom filter for shown items: BFFM_IUNKNOWN/IFolderFilter

$
0
0

It's possible to have complete control over what items are shown in the SHBrowseForFolder dialog. The picture above shows a filter of *.exe applied to a dialog with the BIF_BROWSEINCLUDEFILES option, but you can filter in a wide variety of ways as the IShellFolder and pidl for each item is passed, allowing you to get the name and compare by string and properties, as in the demo, or anything else you could want. The project notes where you could even filter by SHCONTF options.
This is accomplished through the BFFM_IUNKNOWN message that is received in the callback function. A lot of places have mentioned what it's for, but I wanted to show the actual details of using that message to set up a filter.

First, you create a class module that implements the IFolderFilter interface and create an instance of it before calling the dialog. The GetEnumFlags method is where you can filter by SHCONTF, but this demo is mainly concerned with examining each item in the ShouldShow method. Whether to show the item or not is based on the return code, so the class module function is swapped out to a function in the module. Here's the demo filters files, but not folders, according to the pattern specified in the text box:
Code:

Public Function ShouldShowVB(ByVal this As IFolderFilter, ByVal psf As IShellFolder, ByVal pidlFolder As Long, ByVal pidlItem As Long) As Long
Dim psi As IShellItem
Dim lpName As Long, sName As String
Dim dwAtr As Long
On Error GoTo e0

SHCreateItemWithParent 0&, psf, pidlItem, IID_IShellItem, psi
If (psi Is Nothing) = False Then
    psi.GetAttributes SFGAO_FILESYSTEM Or SFGAO_FOLDER, dwAtr
    If ((dwAtr And SFGAO_FILESYSTEM) = SFGAO_FILESYSTEM) And ((dwAtr And SFGAO_FOLDER) = 0) Then 'is in normal file system, is not a folder
        psi.GetDisplayName SIGDN_PARENTRELATIVEPARSING, lpName
        sName = LPWSTRtoStr(lpName)
        Debug.Print "ShouldShow?" & sName & "|" & gSpec
        If PathMatchSpecW(StrPtr(sName), StrPtr(gSpec)) Then
            ShouldShowVB = S_OK 'should show
        Else
            ShouldShowVB = S_FALSE 'should not show
        End If
    End If
Else
    Debug.Print "ShouldShow.NoItem"
End If

Exit Function
e0:
Debug.Print "ShouldShowVB.Error->" & Err.Description
End Function

Now that the filter object and routine are good to go, it needs to be assigned to the dialog. When the BFFM_IUNKNOWN message fires, the lParam contains a pointer to an IUnknown object which implements IFolderFilterSite, which contains the call to assign our filter class. If the messages fires but the object is Nothing, the filter class needs to be released and reset, otherwise a subsequent call to SHBrowseDialog won't be filtered.
Code:

Public Function BrowseCallbackProc(ByVal hWnd As Long, ByVal uMsg As Long, ByVal lParam As Long, ByVal lpData As Long) As Long
Dim pSite As IFolderFilterSite
Dim pUnk As oleexp.IUnknown

Select Case uMsg

    Case BFFM_IUNKNOWN
        'lParam contains a pointer to an IUnknown that implements IFolderFilterSite
        Debug.Print "Received BFFM_IUNKNOWN"
        vbaObjSetAddRef pUnk, lParam
        Debug.Print "Set obj"
        If (pUnk Is Nothing) = False Then
            Set pSite = pUnk
            If (pSite Is Nothing) = False Then
                Debug.Print "Setting filter"
                pSite.SetFilter cFilter
                Debug.Print "Filter set"
            Else
                Debug.Print "Failed to set pSite"
            End If
        Else
            Debug.Print "Failed to set pUnk"
            Set cFilter = Nothing
        End If
End Select
End Function

And that's about it. The rest is just calling the dialog like normal (+making a new instance of the cFolderFilter class first).

Requirements
-The demo project requires Windows Vista or newer, although it could theoretically be reworked to support XP.
-oleexp 4.1 or newer (this project requires a bug fixed only in 4.1, not 4.0)
-mIID.bas (included in the oleexp download)
Attached Files

Resizeable VB6 UserForms (and Unicode Form Captions)

$
0
0
This system uses a small amount of code in a form module plus a small class module shared by all of your forms to enable you and the user to be able to move and size any form and have all of the controls on the form resize properly. In summary:


  • Form and control resizing is available (including use of the maximize button) all of the time for all new and existing forms. This requires 2 variable declarations and 4 lines of code in each form.
  • The programmer has control over whether resize is allowed, what controls will resize, whether the form retains its height to width ratio as it resizes, etc.
  • Form sizing routines are available to make a form a certain percentage of the screen size, regardless of the screen resolution and size.
  • A form can be maximized without getting distorted.
  • A form’s last size and position can be saved and then restored the next time the form is used. This data can be saved to a file or to the registry. This takes only 10 lines of code for each form.
  • As a bonus, you can now easily set the form title (caption) with any Unicode string you want.
  • Minimal use of Windows API (2 calls).



Class Module

In a form that you want to be resizable you should put the following code (cut and paste if necessary). There are more optional things you can add that will be discussed later but the simplest code to provide comprehensive resizing requires only the following a few lines of code in your form (not clResizer):

First, ensure that the BorderStyle property of the form is set to “2 – Sizable”.

In the declaration section which is below the Option Explicit statement (which you definitely should be using):

Code:

Private frmResize As New clResizer
Public UniCaption As String

Then if you have a Form_Load procedure (if not, make one), put this line in it:

Code:

frmResize.NewSetup Me ' put after any form setup code in this subroutine
That’s all that is required to have form resizing that also makes all of the controls on your form resize along with the form and to prevent everything on the form from being distorted by keeping the form’s height/width aspect ratio the same as the original form.

Normally you will want the form to appear in the same size and position on the user’s screen each time the form is displayed. We will cover various other options later but to have automatic save and restore of your form’s size and position, modify the Form_Load routine as shown below and modify or add the Form_Unload routine as shown below.

Code:

Private Sub Form_Load()
frmResize.NewSetup Me ' put after any form setup code in this subroutine
If frmResize.GetFormParmsFromFile(App.Path) = 0 Then ' specify "" to read from the registry
  ' either first time through (file does not exist) or had read error
  frmResize.CenterForm 30, 30, True ' center form on screen and make 30% the width of screen
  frmResize.MoveForm 5, 5, False
  End If
End Sub
 
 
Private Sub Form_Unload(Cancel As Integer)
' put any other code you need to save data from the form here
If Not Cancel Then
  ' This is our last shot at the form before unloading. It is possible that you
  '  have code to just hide the form and in that case we don't need to save the
  '  form settings because sometime later before the program ends this Form_Unload
  '  routine will be called.
 
  ' Don't write to App.Path in a real EXE, Windows no longer allows writing files
  ' to the Program Files folders
  frmResize.SaveFormParms2File App.Path ' specify "" to write to the registry
  End If
End Sub


Displaying a Unicode Caption

There are several peope and companies who provide Unicode controls to put onto a Form but there is no native way of putting a Unicode title on the base Form itself. This is especially frustrating since VB6 deals with Unicode directly. The problem is that the IDE editor doesn’t do Unicode nor does the code that sets up the form when it is displayed. There is now a public variable in each form called UniCaption. If you set this variable to any string then when the form is displayed UniCaption will be the form caption instead of whatever was used previously as Form.Caption. If you leave this variable blank then whatever you had set as the Form.Caption is used for the Form caption.

Suppose you have a form named fmTestMe. Suppose you want the string “あいうえお Caption” to be displayed as the caption of fmTestMe. If it never changed you could put the following line in the Form_Load sub of the form:

Code:

UniCaption = ChrW$(&H3042) & ChrW$(&H3044) & ChrW$(&H3046) & ChrW$(&H3048) & _
ChrW$(&H304A) & " Caption"

Alternatively, you can set the variable from a normal module or another class module or form by specifying the form name (the following snippet assumes the form name is TestForm):

Code:

TestForm.UniCaption = ChrW$(&H3042) & ChrW$(&H3044) & ChrW$(&H3046) & ChrW$(&H3048) & _
ChrW$(&H304A) & " Caption"

If the value of UniCaption is set before anything else is done with the form then the code you put in the Form_Load routine that calls NewSetup not only sets the size and location of your form, it also takes the value for UniCaption and sets the form caption with it.

But suppose you want to change the caption one or more times after it has been displayed. First, put the following simple routine in your form code so you can get to the variable and procedure in the class module:

Code:

Public Sub SetUnicodeCaption(Caption As String)
UniCaption = Caption
frmResize.ShowUniCap
End Sub

And then whenever you want to change it after the form has been displayed you would call it like this (if the form name was TestForm and the new Caption string was the variable MyNewCaption):

TestForm.SetUnicodeCaption(myNewCaption)


Form Design Considerations

Fonts

Since the objective of this system is to enable making your forms larger and smaller with corresponding change in the size and fonts of each control, you should avoid the use of raster fonts in your forms since these scale extremely poorly. Typical Windows raster fonts include:

8514oem Regular
ADMUI3Lg Regular
ADMUI3Sm Regular
Courier Regular (there is a TrueType version called Courier New that is okay)
Fixedsys Regular
Modern Regular
MS San Serif Regular (this is the VB6 standard font)
MS Serif Regular
Roman Regular
Script Regular
Small Fonts Regular
System Bold
Terminal

List/Combo Boxes
Drop-down boxes appear to size properly. If you use a list or combo box that shows more than one items at a time, it is possible that as you resize the form the text at the bottom gets dropped off and a vertical scrollbar appears. That’s because these controls size their own fonts based on the vertical size of the box. I have rarely seen this behavior be any problem but when I did I just set the control’s IntegralHeight property to False and gave a tiny bit more room at the bottom of the control.

Setting the Form’s Initial Size and Position

Below are some techniques for setting the initial position of the form. My recommendation is to initially put the form on the same screen as VB and since it is resizable and moveable the user will put it wherever it works best and then we will save and restore that size and position for future re-use.

Because of the way forms work, once it is displayed the programmer has little control over the position of the form. Generally you will be more concerned about what the user does to items on the form and you won’t be too worried about where the form is or how large it is as long as the user can put it wherever he/she likes and can make it as large or small as desired.

Programatically we can respond to the Resize event (which we already do) but that is largely driven by the user who is resizing the form. I suppose you could catch this re-sizing event and do something different but I don’t know what. There is no easy way to catch a Move event and the whole purpose of this system is to let the user move the form and re-size as he/she sees fit. So this means that in general we would want to move and or re-size the form just before it is being displayed via the Form.Show command.

You can put code in the Initialize event for the form but keep in mind that at this point we have not yet had Windows make the form resizable so any attempt to resize the form will not work. Also, if you try something like the following in another module it will not work either:

Code:

fmTestMe.CenterForm(50, 50, True)
fmTestMe.Show

Anything in a normal module before the Show command basically causes the Initialize event to fire and our code will be executing before the Windows call to enable resizing. The resizing code is called in the Load event which is after Initialize and just before the form is displayed.

The only way I know of to get code to affect the form after the Show statement is if the form has been Hidden instead of Unloaded.

My recommendation is to decide what you want to do regarding the form size and location and put the code to do this in the Load event procedure in the code for the Form. You have 3 routines you in the Class module for form location that enable you set the size and position to be centered or anywhere on the screen and with little effort you can derive many others. The code to access these 3 routines will need to be in your Form module code.

Code:

Sub CenterForm(WidthPerCent As Single, HeightPerCent As Single, Limit2Screen As Boolean)
This class module sub enables you to center and optionally resize the form.


  • To size the form based on the available screen width and height
    • WidthPerCent and HeightPerCent are the %'s of the screen width and height respectively
    • To make a form fill up half of the screen width regardless of the screen size and resolution you would specify the following in the form’s Load procedure:


Code:

frmResize.CenterForm 50, 50, True
  • Note – As long as Zoomable is True (default), the setting for HeightPerCent is ignored because the code determines the required height to keep the height/width ratio constant.





  • To size the form based on the original size of the form
    • WidthPerCent and HeightPerCent are based on the original form size but negative
    • To make a form be twice the size of the original form you would specify the following in the Load procedure of your form:


Code:

frmResize.CenterForm -200, -200, True  ' for 200% but negative
  • Note – As long as Zoomable is True (default), the HeightPerCent parameter is ignored.


  • If limit2Screen is True then the form size is adjusted as necessary to keep it all onscreen.



Code:

Sub MoveForm(WidthPerCent As Single, HeightPerCent As Single, Limit2Screen As Boolean)
This class module sub enables you to move the form and optionally keep it onscreen.


  • To move the form based on the available screen width and height
    • WidthPerCent and HeightPerCent are the %'s of the screen width and height respectively
    • To make a form’s upper left corner go to the middle of the screen regardless of the screen size and resolution you would specify the following in the form’s Load procedure:


Code:

frmResize.MoveForm 50, 50, True
  • Note – As long as Zoomable is True (default), the setting for HeightPerCent is ignored because the code determines the required height to keep the height/width ratio constant.



  • To move the form to the specific left and top coordinates
    • WidthPerCent and HeightPerCent are the specific form position values for Left and Top
    • To make a form go to the top left of the screen you would specify this in the Load procedure:


Code:

Frmresize.MoveForm 0, 0, True
  • Note – As long as Zoomable is True, the HeightPerCent parameter is ignored.


  • If limit2Screen is True then the form size is adjusted as necessary to keep it all onscreen.


So if we wanted to make our form be 65% the width of the screen (whatever that may be) and also displayed with the upper left corner 5% of the screen width and height from the screen’s upper left corner we could have a Form_Load routine that looks like this:

Code:

Private Sub Form_Load()
frmResize.NewSetup Me
frmResize.CenterForm 65, 65, True ' center form on screen and make 65% the width of screen
frmResize.MoveForm 5, 5, False
End Sub


Continued below...
Attached Files

VB6 - NAT Traversal

$
0
0
A NAT router is an excellent way to protect your computer network from outside hackers. The normal way to allow an outside host to connect with an internal host sitting behind a NAT router is to manually add a port forwarding address to the router setup. Setting up a router is not a simple task for the average user, and some routers have restricted access (especially public WiFi routers). To understand how to allow a host to connect with your program through a NAT router without adjusting the router setup, you must first understand how a NAT router works. Since we are interested in TCP connections, we will restrict our discussion to this type of connection.

All outbound connection requests (SYN request) are allowed through the router. At this point, the connection and it's translation are added to a NAT table. Your computer initiated the request using it's private IP address (eg. 192.168.1.5) and it's first available port (eg. 50342). The NAT router does the same thing. It uses the public address (eg. 201.34.87.52) and it's first available port (eg. 54671) and translates the outbound request to use these values. The other end only sees the router values. It never sees the values that your computer used. The values added to the NAT table on a SYN request include all 4 values:
Private IP Private Port Public IP Public Port
192.168.1.5 50342 201.34.87.52 54671
Most routers will allow about 60 seconds for this initial connection request to be acknowledged. Otherwise, it times out and is removed from the table. The connection acknowledgement (SYN-ACK) from the other end is received by the router, translated back to the private values, and forwarded to your computer. Once the connection is established, the inactivity timeout is much longer (say 24 hours), but will vary with the router.

So to traverse a NAT router without changing it's settings, we must create the NAT table entry and then connect to it before it times out. To accomplish this, we use a third party server which supplies the connecting IP address and port. Seems simple enough, but there is a complication. TCP standards do not allow us to share a port. So we will just close the existing connection and open it again with the same port number. There are 2 problems with this approach. One is that there is a TIME_WAIT after a connection is closed, which is to allow for straggling packets to be received.The other issue is that the newer versions of Microsoft sockets don't allow us to pick and choose the internal port number on a TCP connection request. It automatically chooses the first available port. So I set out to find a resolution to these issues.

To enable each side to connect to each other, we must kill the connection to the server without leaving the socket in a TIME_WAIT state. To do this, we set the "SO_LINGER" & "SO_REUSEADDR" options when we connect to the server. When we receive instructions from the server with the IP address & port number of the other end, we kill the existing socket and initiate a connection request using the same local IP address and port to the remote IP address and port. That creates a temporary NAT Table entry in the router.

When both sides are connected to the server, the server sends to each side the IP and port from the other side. Both ends will kill the existing socket that connects it to the server, create a new socket on the same local IP & port, and send a connection request to the other end. Don't ask me how this works, but the first one to receive the SYN request sends a SYN-ACK to the other end. This is enough to establish a connection and extend the timeout on the NAT router. Both ends will just have a connected socket (no listening socket).

So why are we going to this length to establish a connection? We want to allow a direct connection between 2 parties without the necessity of having the server forward all the information from both ends (as in a proxy server). The only purpose of the server is to supply the necessary information to establish that connection. Once directly connected, the entire session can be encrypted, and the server has no record of it.

The attached programs (NAT.vbp & Server.vbp) allow us to test this functionality. The server is set up to listen on port 24 using "SimpleServer.cls". Being a server, firewalls and routers must be setup to allow outside connections on port 24. To test "NAT.vbp", I added a NAT router between one of my computers and the local network, and a different NAT router to a second computer. This created a double NAT situation between these 2 particular computers, but a single NAT between each computer and the rest of the local network. The server program (Server.vbp) I set up on the local network. That left all 3 computers on separate networks. As each Nat.vbp connects to the server program, the server will display the connecting IP address and port. Machines operating from behind a NAT router will display the IP address and port of the NAT router public interface, instead of the computer's local IP address and port. Then the Trigger button on the server was clicked to send instructions to the 2 test computers. It took about 1 second for the connection to be established between the 2 test computers.

I had a great deal of difficulty getting this to work because of the vintage of one of the routers. Most routers will assign the same public port to a connection when it is using the same Private IP address and port. The older router however incremented the Public port number with each connection. I got around this problem by adding 1 to "sAddr" in the "SndConnect" routine of the machine behind the newer router.
Code:

mSocket.TCPConnect(sAddr, lPort + 1, PortListen)
You can test how your own router behaves by connecting to the server program from behind the router and noting the port number displayed in the TextBox on the server. Then disconnect and connect again. It should be the same each time.

J.A. Coutts
Attached Images
 
Attached Files

Compression in VB6: modern solutions

$
0
0
Compression has always been a cumbersome task in VB. Microsoft provides very weak support in their standard libraries, and while there are good 3rd-party solutions for VB developers, they are either expensive (http://www.dart.com/zip-activex-library-api.aspx) or very difficult to use correctly (http://www.7-zip.org/sdk.html).

So for many years, VB6 developers have fallen back on the classic zLib compression library (http://zlib.net/). zLib is an open-source compression library with a very permissive license, and it is "good enough" for most tasks: decent compression ratios, but with relatively slow compression and decompression speeds.

But in recent years, even zLib has become problematic for VB6 users. The stdcall variant of zLib hasn't been updated in over a decade, and it contains serious known security bugs. You can always compile your own version of zLib from the latest source code, but the core library definitions are bugged, so this requires a fairly deep knowledge of C and a lot of patience. (Also, zLib's source code hasn't been updated in over three years, and there are a huge number of bug fixes that have yet to be incorporated.)

And even if you do manage to survive all this and successfully build a recent version of zLib, you're still left with compression technology that is 20+ years old. A ton of compression research has been done since 1995 (when zLib first released), and we now have libraries that are both much faster, and with even better compression ratios.

So here's what this small project does: it provides a small "Compression" module that wraps four different open-source compression libraries: zLib, zstd, lz4, and lz4_hc. The compression/decompression functions are unified so you simply call a function like "Compress", and pass a "compression library enum" that specifies which compression engine you want to use.

To simplify this demo, precompiled DLLs are provided for each library. Because these are all based off open-source projects (links to code below), I believe these still meet the vbforums requirements for precompiled binaries. You are of course free to compile these yourself, from the latest source code, but you will need a modern copy of Visual Studio, some knowledge of compiling C code, and you must manually modify the project files to build stdcall variants. (They all default to cdecl, as-is.)

These are all bare C libraries, so they do not need to be registered on target PCs. Simply ship them in a subfolder of your project - for example, this demo project uses a "\Plugins\" subfolder, and the DLLs are all loaded at run-time via LoadLibrary.

Here is a brief overview of the provided compression libraries, all of which are 100% open-source and free to use in personal or commercial projects (with attribution - see the included license files for details).

- zLib is the classic library you know and love. I've freshly compiled the newest build (v1.2.8) for this demo. Despite its age, zLib remains a solid general-purpose compression library, with good compression ratios across a wide variety of data, but with slow compression speeds compared to the competition. zLib supports a "compression level" parameter that allows you to choose a trade-off between faster but worse compression, or slower but better compression. Generally speaking, there is no longer much reason to use zLib, unless you specifically need the DEFLATE algorithm it provides (e.g. to work with .gz files).

- zstd (or "zstandard") is a modern replacement for zLib. It was originally developed by Yann Collet, and its ongoing development is now sponsored by Facebook. It is 100% open-source and BSD licensed. zstd is significantly faster than zLib at both compression and decompression, and it also achieves better compression ratios. It provides a "compression level" parameter just like zLib, but with a much wider range, including extremely slow speeds but extremely good compression ratios if you need that sort of thing. For most users, zstd could replace zLib in their existing projects, and they'd immediately get a "free" performance boost from it.

- lz4 is a real-time compression engine that emphasizes performance above all else. It was also developed by Yann Collet, and it is also 100% open-source and BSD licensed. lz4 is so fast that it is now used for OS-level compression (Linux), file system compression (OpenZFS, SquashFS), database compression (MySQL), RAM caching (Emscripten, ZRam), and a whole bunch of video games (Battlefield 4, Black Ops 3, etc). LZ4's speed comes at a trade-off, however - it does not compress as well as zLib or zstd on most data. It also provides an adjustable "compression level" parameter, but instead of providing "slower but better" compression as you increase this value, lz4 provides "faster but worse" compression. It is the best solution when speed is paramount. (For example, lz4 is one of the few algorithms fast enough to provide a performance benefit vs raw uncompressed data when reading/writing to a hard drive.)

- lz4_hc comes "for free" with lz4. It is a "high-compression" variant of lz4, with much better compression ratios but much slower compression speeds. Decompression speed remains the same. It is a good solution if you have all the time in the world for compression, but you still require very fast decompression. (This is the version that video games use, for example.)

The included demo project allows you to compare compression speed, decompression speed, and compression ratio across all libraries. A baseline comparison of "no compression" is also provided, which measures timing against bare RtlMoveMemory calls. I've included a few multilanguage XML files for comparison (because they're small enough to fit inside vbforum size limits), but for best results, you should test some of your own files. Just drag-and-drop a file onto the project window to run an automated test across all libraries.

Note that - by design - the Compression module operates entirely on byte arrays and/or bare pointers (passed using VarPtr()). This makes it trivial to compress source data of any size or type. Specialized functions for Strings or other data types could always be added, but for now, those are left as an exercise to the reader.

Bug reports and feedback welcome, of course.

Download here:
Compression.zip
Attached Files

Custom Scrollbar (vbRichClient)

$
0
0
As requested here http://www.vbforums.com/showthread.p...using-Pictures

This is a custom scrollbar class that requires only to be given a reference to a picture box, as illustrated in this demo. The same class supports both horizontal and vertical orientations.

It is 99% compliant to a regular scrollbar; the ony real exception is that it's Change and Scroll events also report the current value of the scrollbar. In the case of the latter, it actually reports an exact value (e.g. 6.72, rather than 7), which can be useful for smooth-scrolling type effects. Or you can just use Round(ExactValue) in the event handler if you prefer to not have this level of precision.

Requires a reference to Olaf's vbRichClient5.dll
Attached Files

Send email in a simple VB6 project using a command1 button gmail smtp

$
0
0
i want a project code example using command1 button to submit a email using gmail smtp to submit a email to my destinated gmail email address using a single click
Viewing all 1483 articles
Browse latest View live


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