Related
I want to create an application that allows users to have different icons on different Windows 10 desktops. I can deal with the switching of virtual desktops using the Window Station and Desktop functions and the Virtual Desktop Shell Interface. So now I know how to detect that a desktop has been switched and I need to change the location of the Desktop User Folder as fast as possible.
I know there are two ways of doing this in the User Interface:
A) Via User Folders' properties
Open %HomePath%/Desktop in Explorer
Right click on background, open Properties
In tab Location type in the new path
Hit OK and then No (as you don't want to move the files)
Sometimes needed: click on desktop, press F2
.
B) Via Registry
Change Desktop in HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders to the new address
Relog
.
The B option would be ideal for a programmatical solution if it wasn't for that relog part. Which is, as you can certainly understand, a bit of a dealbreaker.
The VB.Net code below will save all open desktop folder locations and sizes in the Windows registry and restore them on demand. Run from the command line or a batch file. To save folder attributes add a parameter of 'Set' (no quotes), to restore, no parameter.
Option Explicit On
Imports System.Text
Public Class FixFolders
Structure RECT
Dim Left As Integer
Dim Top As Integer
Dim Right As Integer
Dim Bottom As Integer
End Structure
Structure POINTAPI
Dim x As Integer
Dim y As Integer
End Structure
Structure WINDOWPLACEMENT
Dim length As Integer
Dim flags As Integer
Dim showCmd As Integer
Dim ptMinPosition As POINTAPI
Dim ptMaxPosition As POINTAPI
Dim rcNormalPosition As RECT
End Structure
Private Structure TDeskTopWindow
Dim lhwnd As Integer
Dim WinTitle As String
Dim WinRect As RECT
End Structure
Private Declare Function GetWindowRect Lib "user32" Alias "GetWindowRect" (ByVal hwnd As Integer, ByRef lpRect As RECT) As Integer
Declare Function MoveWindow Lib "user32" (ByVal hwnd As Int32, ByVal x As Int32, ByVal y As Int32, ByVal nWidth As Int32, ByVal nHeight As Int32, ByVal bRepaint As Int32) As Int32
Declare Function GetDesktopWindow Lib "user32" () As Int32
Declare Function GetClassName Lib "user32" Alias "GetClassNameA" (ByVal hwnd As Int32, ByVal lpClassName As String, ByVal nMaxCount As Int32) As Int32
Private Delegate Function EnumChildWindowsCallback(ByVal hWnd As IntPtr, ByVal lParam As IntPtr) As Boolean
Private Declare Function EnumChildWindows Lib "user32" (ByVal hWnd As IntPtr, ByVal lpEnumFunc As EnumChildWindowsCallback, ByVal lParam As IntPtr) As Boolean
Private Delegate Function EnumWindowsProcDelegate(ByVal hWnd As IntPtr, ByVal lParam As IntPtr) As Boolean
Private Declare Function EnumWindows Lib "user32" (ByVal lpEnumFunc As EnumWindowsProcDelegate, ByVal lParam As IntPtr) As Boolean
Private Declare Auto Function GetWindowText Lib "user32" (ByVal hWnd As IntPtr, ByVal lpString As StringBuilder, ByVal nMaxCount As Integer) As Integer
Private Declare Function SetFocusAPI Lib "user32" Alias "SetFocus" (ByVal hwnd As Int32) As Int32
Private Declare Function SetWindowPlacement Lib "user32" (ByVal hwnd As Integer, ByRef lpwndpl As WINDOWPLACEMENT) As Integer
Private Declare Function SetForegroundWindow Lib "user32" (ByVal hwbd As Int32) As Int32
'Private Declare Function FlashWindow Lib "User32" (ByVal hWnd As Int32, ByVal Invert As Int32) As Int32
Private Declare Function SetActiveWindow Lib "User32" (ByVal hWnd As Int32) As Int32
Private Shared lpwndplNew As WINDOWPLACEMENT
Private Const SW_SHOWMINIMIZED As Short = 2
Private Const SW_SHOWMAXIMIZED As Short = 3
Private Const SW_SHOWNORMAL As Short = 1
Private Shared Mode As String
Private WindowArray() As String
Private Shared DeskTopWindows() As TDeskTopWindow, DTWinIndex As Integer
Public Shared Sub Main()
Dim left, Right, Top, Bottom As Integer
Dim DTWinIndex As Integer
Dim RetVal1, RetVal2, RetVal3, TimeOut As Integer
Mode = Command()
GetDeskTopFolderWindows()
For DTWinIndex = 0 To DeskTopWindows.GetUpperBound(0)
Select Case Mode
Case ""
With DeskTopWindows(DTWinIndex)
'If it' s not there, put it in
If GetSetting("FixFolders", .WinTitle, "Left") = "" Then
SaveSetting("FixFolders", .WinTitle, "Left", .WinRect.Left.ToString)
SaveSetting("FixFolders", .WinTitle, "Right", .WinRect.Right.ToString)
SaveSetting("FixFolders", .WinTitle, "Top", .WinRect.Top.ToString)
SaveSetting("FixFolders", .WinTitle, "Bottom", .WinRect.Bottom.ToString)
End If
left = Val(GetSetting("FixFolders", .WinTitle, "Left"))
Right = Val(GetSetting("FixFolders", .WinTitle, "Right"))
Top = Val(GetSetting("FixFolders", .WinTitle, "Top"))
Bottom = Val(GetSetting("FixFolders", .WinTitle, "Bottom"))
While .WinRect.Bottom <> Bottom Or .WinRect.Left <> left Or .WinRect.Right <> Right Or .WinRect.Top <> Top
'RetVal1 = SetForegroundWindow(lhWnd)
RetVal2 = SetWindowPlacement(.lhwnd, lpwndplNew) 'This 'restores' the window if minimized
RetVal3 = MoveWindow(.lhwnd, left, Top, Right - left, Bottom - Top, True)
' Log.WriteLine(Now.TimeOfDay.ToString.Substring(0, 8) & " Set " & .WinTitle)
RetVal1 = GetWindowRect(.lhwnd, .WinRect) ' get current size
TimeOut += 1
If TimeOut > 1 And TimeOut < 10 Then Threading.Thread.Sleep(1000)
End While
End With
Case "Set"
With DeskTopWindows(DTWinIndex)
SaveSetting("FixFolders", .WinTitle, "Left", .WinRect.Left.ToString)
SaveSetting("FixFolders", .WinTitle, "Right", .WinRect.Right.ToString)
SaveSetting("FixFolders", .WinTitle, "Top", .WinRect.Top.ToString)
SaveSetting("FixFolders", .WinTitle, "Bottom", .WinRect.Bottom.ToString)
End With
End Select
Next
End Sub
Private Shared Sub GetDeskTopFolderWindows()
Dim lhwnd As Integer, lParam As IntPtr
DTWinIndex = -1
lhwnd = GetDesktopWindow() ' Find the Desktop's Child Windows
EnumChildWindows(lhwnd, AddressOf EnumChildProc, lParam)
End Sub
Shared Function EnumChildProc(ByVal lhWnd As IntPtr, ByVal lParam As IntPtr) As Boolean
Dim RetVal1 As Int32
Dim WinClassBuf As String
Dim WinTitleBuf As New StringBuilder(256)
Dim WinClass As String
Dim WinRect As RECT
WinClassBuf = New String(Chr(0), 256)
WinTitleBuf.Append(Chr(0), 256)
RetVal1 = GetClassName(lhWnd, WinClassBuf, WinClassBuf.Length)
WinClass = WinClassBuf.ToString
WinClass = StripNulls(WinClassBuf) ' remove extra Nulls & spaces
If WinClass = "CabinetWClass" Or WinClass = "ExploreWClass" Then ' TextBox Window
DTWinIndex += 1
ReDim Preserve DeskTopWindows(DTWinIndex)
DeskTopWindows(DTWinIndex).lhwnd = lhWnd
GetWindowText(lhWnd, WinTitleBuf, WinTitleBuf.Capacity)
DeskTopWindows(DTWinIndex).WinTitle = WinTitleBuf.ToString
RetVal1 = GetWindowRect(lhWnd, WinRect) ' get current size
DeskTopWindows(DTWinIndex).WinRect = WinRect
End If
EnumChildProc = True
End Function
Public Shared Function StripNulls(ByVal OriginalStr As String) As String
' This removes the extra Nulls so String comparisons will work
If (InStr(OriginalStr, Chr(0)) > 0) Then
'OriginalStr = Left(OriginalStr, InStr(OriginalStr, Chr(0)) - 1)
OriginalStr = OriginalStr.Substring(0, OriginalStr.IndexOf(Chr(0)))
End If
StripNulls = OriginalStr
End Function
End Class
I'm trying to retrieve the text from an EDIT control using GetWindowText and GetWindowTextLength. The application retrieves the text from the window under the cursor and it works on all windows with a caption or text with the exception of the EDIT control. The EDIT control is the result window on the Windows XP Calculator, calc.exe.
Dim S As String
Dim L As Long
L = GetWindowTextLength(handle) + 1
Receiving string = GetWindowText(handle, S, L)
EDIT:
According to SPY++ the Edit class control does not receive the EM_GETSELTEXT or the WM_GETTEXT message.The code below retrieves the text from the Edit class control on the Windows XP calc.exe calculator every time that I press a button on my UI. It is not the method that I would have preferred to use, however, it accomplishes my task.
Const EM_SETSEL = &HB1
Const ES_READONLY = &H800
Const WM_COPY = &H301
Const EM_GETSELTEXT = &H43E
Const WM_GETTEXTLENGTH = &HE
Const WM_SETFOCUS As Long = &H7
Dim L As Long
L = SendMessage(EditHwnd, WM_GETTEXTLENGTH, 0&, 0)
SendMessage EditHwnd, WM_SETFOCUS, 0&, 0
SendMessage EditHwnd, EM_SETSEL, 0&, L
SendMessage EditHwnd, ES_READONLY, 0&, 0 ' read only = false
Clipboard.Clear
SendMessage EditHwnd, WM_COPY, 0&, 0
SendMessage EditHwnd, ES_READONLY, 1&, 0 ' read only = true
Receiving string = Clipboard.GetText
Clipboard.Clear
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Private Declare Function GetWindowTextLength Lib "user32" Alias "GetWindowTextLengthA" (ByVal hwnd As Long) As Long
Private Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" (ByVal hwnd As Long, ByVal lpString As String, ByVal cch As Long) As Long
Function GetText(handle As Long) As String
Dim S As String, L As Integer, cch As Long
L = GetWindowTextLength(handle) + 1
S = String(L, 0)
GetText = Mid(S, 1, GetWindowText(handle, S, L))
End Function
Private Sub Form_Load()
Dim hw As Long
hw = Me.hwnd
MsgBox GetText(hw)
End Sub
But it will not work with controls like EDIT, as it is written in help. ;( In order to get the text of the child window (control), try to get a list of all windows using API EnumWindows/EnumThreadWindows/GetWindowThreadProcessId, to find the desired control.
What is the name of the class to EDIT You need? What is its width and height? I could write code specifically for the search for this control.
this code works fine in Windows XP (virtual machine)
Private Declare Function FindWindow Lib "user32.dll" Alias "FindWindowA" _
(ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Private Declare Function FindWindowEx Lib "user32.dll" Alias "FindWindowExA" _
(ByVal hWnd1 As Long, ByVal hWnd2 As Long, ByVal lpsz1 As Any, ByVal lpsz2 As Any) As Long
Private Declare Function SendMessage Lib "user32.dll" Alias "SendMessageA" _
(ByVal hWnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByRef lParam As Any) As Long
Private Declare Function AttachThreadInput Lib "user32.dll" _
(ByVal idAttach As Long, ByVal idAttachTo As Long, ByVal fAttach As Long) As Long
Private Const WM_GETTEXT As Long = &HD&
Private Const WM_GETTEXTLENGTH As Long = &HE&
Function WindowText(ByVal hWnd As Long) As String
Dim ret As Long
ret = SendMessage(hWnd, WM_GETTEXTLENGTH, 0, ByVal 0&)
WindowText = String(ret, 0)
ret = SendMessage(hWnd, WM_GETTEXT, ret + 1, ByVal WindowText)
End Function
Private Sub Command1_Click()
Dim hCalc As Long, hEdit As Long
hCalc = FindWindow("SciCalc", vbNullString)
hEdit = FindWindowEx(hCalc, 0&, "Edit", vbNullString)
MsgBox WindowText(hEdit)
End Sub
Is there a way to get the equivalent of VB's SelStart and SelLength properties from an Internet Explorer_Server object?
I've tried using SendMessage EM_GETSEL but it doesn't return anything.
You have to query the InternetExplorer_Server's HWND for its IHTMLDocument2 interface, and then you can use the browser's DOM interfaces to manipulate the browser content as needed:
How to get IHTMLDocument2 from a HWND
IHTMLDocument2::selection Property
Use this code where you want to calculate SelStart and SelLength. Replace InternetExplorer_Server.hWnd with a handle to your object.
Dim DomObj As IHTMLDocument2
Dim SelObj As IHTMLTxtRange
Set DomObj = IEDOMFromhWnd(InternetExplorer_Server.hWnd)
Set SelObj = DomObj.selection.createRange
TextToCheck$ = DomObj.body.innerText
' Calculate SelLength...
SelLength = Len(SelObj.Text)
SelObj.moveStart "character", -Len(TextToCheck$)
' Calculate SelStart...
SelStart = Len(SelObj.Text)
Requires the following code in a Module:
Private Type UUID
Data1 As Long
Data2 As Integer
Data3 As Integer
Data4(0 To 7) As Byte
End Type
Private Declare Function RegisterWindowMessage Lib "user32" Alias "RegisterWindowMessageA" (ByVal lpString As String) As Long
Private Declare Function SendMessageTimeout Lib "user32" Alias "SendMessageTimeoutA" (ByVal hwnd As Long, ByVal msg As Long, ByVal wParam As Long, lParam As Any, ByVal fuFlags As Long, ByVal uTimeout As Long, lpdwResult As Long) As Long
Private Declare Function ObjectFromLresult Lib "oleacc" (ByVal lResult As Long, riid As UUID, ByVal wParam As Long, ppvObject As Any) As Long
Public Function IEDOMFromhWnd(ByVal hwnd As Long) As IHTMLDocument2
Dim IID_IHTMLDocument2 As UUID
Dim hWndChild As Long
Dim lRes As Long
Dim lMsg As Long
Dim hr As Long
' Register the message
lMsg = RegisterWindowMessage("WM_HTML_GETOBJECT")
' Get the object pointer
Call SendMessageTimeout(hwnd, lMsg, 0, 0, SMTO_ABORTIFHUNG, 1000, lRes)
If lRes Then
' Initialize the interface ID
With IID_IHTMLDocument2
.Data1 = &H626FC520
.Data2 = &HA41E
.Data3 = &H11CF
.Data4(0) = &HA7
.Data4(1) = &H31
.Data4(2) = &H0
.Data4(3) = &HA0
.Data4(4) = &HC9
.Data4(5) = &H8
.Data4(6) = &H26
.Data4(7) = &H37
End With
' Get the object from lRes
hr = ObjectFromLresult(lRes, IID_IHTMLDocument2, 0, IEDOMFromhWnd)
End If
End Function
I need to have my form-based application check stdin periodically for input, but still perform other processing. Scripting.TextStream.Read() and the ReadFile() API are blocking, is there a non-blocking method of reading stdin in VB6?
With Timer1 set to fire every 100 ms, I've tried:
Private Declare Function AllocConsole Lib "kernel32" () As Long
Private Declare Function FreeConsole Lib "kernel32" () As Long
Dim sin As Scripting.TextStream
Private Sub Form_Load()
AllocConsole
Dim FSO As New Scripting.FileSystemObject
Set sin = FSO.GetStandardStream(StdIn)
Timer1.Enabled = True
End Sub
Private Sub Timer1_Timer()
Dim cmd As String
While Not sin.AtEndOfStream
cmd = sin.Read(1)
Select Case cmd
' Case statements to process each byte read...
End Select
Wend
End Sub
I've also tried:
Private Declare Function AllocConsole Lib "kernel32" () As Long
Private Declare Function FreeConsole Lib "kernel32" () As Long
Private Declare Function GetStdHandle Lib "kernel32" (ByVal nStdHandle As Long) As Long
Private Declare Function ReadFileA Lib "kernel32" Alias "ReadFile" (ByVal hFile As Long, lpBuffer As Any, ByVal nNumberOfBytesToRead As Long, lpNumberOfBytesRead As Long, lpOverlapped As Any) As Long
Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
Private Const STD_INPUT_HANDLE = -10&
Dim hStdIn As Long
Private Sub Form_Load()
AllocConsole
hStdIn = GetStdHandle(STD_INPUT_HANDLE)
Timer1.Enabled = True
End Sub
Private Sub Timer1_Timer()
Dim bytesRead as Long
Dim cmd As String
cmd = Space$(16)
cmd = ReadFile(hStdIn, ByVal cmd, Len(cmd), bytesRead, ByVal 0&)
' Statements to process each Line read...
End Sub
I've tried the ReadConsole() API, too, they all block.
Use vbAdvance add-in to compile following sample with "Build As Console Application" option checked.
Option Explicit
'--- for GetStdHandle
Private Const STD_INPUT_HANDLE As Long = -10&
Private Const STD_OUTPUT_HANDLE As Long = -11&
'--- for PeekConsoleInput
Private Const KEY_EVENT As Long = 1
'--- for GetFileType
Private Const FILE_TYPE_PIPE As Long = &H3
Private Const FILE_TYPE_DISK As Long = &H1
Private Declare Function GetStdHandle Lib "kernel32" (ByVal nStdHandle As Long) As Long
Private Declare Function GetConsoleMode Lib "kernel32" (ByVal hConsoleHandle As Long, lpMode As Long) As Long
Private Declare Function SetConsoleMode Lib "kernel32" (ByVal hConsoleHandle As Long, ByVal dwMode As Long) As Long
Private Declare Function PeekNamedPipe Lib "kernel32" (ByVal hNamedPipe As Long, lpBuffer As Any, ByVal nBufferSize As Long, ByVal lpBytesRead As Long, lpTotalBytesAvail As Long, ByVal lpBytesLeftThisMessage As Long) As Long
Private Declare Function ReadFile Lib "kernel32" (ByVal hFile As Long, lpBuffer As Any, ByVal nNumberOfBytesToRead As Long, lpNumberOfBytesRead As Long, ByVal lpOverlapped As Long) As Long
Private Declare Function OemToCharBuff Lib "user32" Alias "OemToCharBuffA" (ByVal lpszSrc As String, ByVal lpszDst As String, ByVal cchDstLength As Long) As Long
Private Declare Function WriteFile Lib "kernel32" (ByVal hFile As Long, lpBuffer As Any, ByVal nNumberOfBytesToWrite As Long, lpNumberOfBytesWritten As Long, lpOverlapped As Any) As Long
Private Declare Function CharToOemBuff Lib "user32" Alias "CharToOemBuffA" (ByVal lpszSrc As String, lpszDst As Any, ByVal cchDstLength As Long) As Long
Private Declare Function PeekConsoleInput Lib "kernel32" Alias "PeekConsoleInputA" (ByVal hConsoleInput As Long, lpBuffer As Any, ByVal nLength As Long, lpNumberOfEventsRead As Long) As Long
Private Declare Function ReadConsoleInput Lib "kernel32" Alias "ReadConsoleInputA" (ByVal hConsoleInput As Long, lpBuffer As Any, ByVal nLength As Long, lpNumberOfEventsRead As Long) As Long
Private Declare Function GetFileType Lib "kernel32" (ByVal hFile As Long) As Long
Sub Main()
Dim hStdIn As Long
Dim sBuffer As String
Dim dblTimer As Double
hStdIn = GetStdHandle(STD_INPUT_HANDLE)
Do
sBuffer = sBuffer & ConsoleReadAvailable(hStdIn)
If dblTimer + 1 < Timer Then
dblTimer = Timer
Call OemToCharBuff(sBuffer, sBuffer, Len(sBuffer))
ConsolePrint "%1: %2" & vbCrLf, Format$(Timer, "0.00"), sBuffer
sBuffer = vbNullString
End If
Loop
End Sub
Private Function ConsoleReadAvailable(ByVal hStdIn As Long) As String
Dim lType As Long
Dim sBuffer As String
Dim lChars As Long
Dim lMode As Long
Dim lAvailChars As Long
Dim baBuffer(0 To 512) As Byte
Dim lEvents As Long
lType = GetFileType(hStdIn)
If lType = FILE_TYPE_PIPE Then
If PeekNamedPipe(hStdIn, ByVal 0, 0, 0, lAvailChars, 0) = 0 Then
Exit Function
End If
End If
If lType = FILE_TYPE_DISK Or lAvailChars > 0 Then
sBuffer = Space(IIf(lAvailChars > 0, lAvailChars, 512))
Call ReadFile(hStdIn, ByVal sBuffer, Len(sBuffer), lChars, 0)
ConsoleReadAvailable = Left$(sBuffer, lChars)
End If
If GetConsoleMode(hStdIn, lMode) <> 0 Then
Call SetConsoleMode(hStdIn, 0)
Do While PeekConsoleInput(hStdIn, baBuffer(0), 1, lEvents) <> 0
If lEvents = 0 Then
Exit Do
End If
If baBuffer(0) = KEY_EVENT And baBuffer(4) <> 0 Then ' baBuffer(4) = INPUT_RECORD.bKeyDown
sBuffer = Space(1)
Call ReadFile(hStdIn, ByVal sBuffer, Len(sBuffer), lChars, 0)
ConsoleReadAvailable = ConsoleReadAvailable & Left$(sBuffer, lChars)
Else
Call ReadConsoleInput(hStdIn, baBuffer(0), 1, lEvents)
End If
Loop
Call SetConsoleMode(hStdIn, lMode)
End If
End Function
Public Function ConsolePrint(ByVal sText As String, ParamArray A() As Variant) As String
' Const FUNC_NAME As String = "ConsolePrint"
Dim lI As Long
Dim sArg As String
Dim baBuffer() As Byte
Dim dwDummy As Long
'--- format
For lI = UBound(A) To LBound(A) Step -1
sArg = Replace(A(lI), "%", ChrW$(&H101))
sText = Replace(sText, "%" & (lI - LBound(A) + 1), sArg)
Next
ConsolePrint = Replace(sText, ChrW$(&H101), "%")
'--- output
ReDim baBuffer(1 To Len(ConsolePrint)) As Byte
If CharToOemBuff(ConsolePrint, baBuffer(1), UBound(baBuffer)) Then
Call WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), baBuffer(1), UBound(baBuffer), dwDummy, ByVal 0&)
End If
End Function
I am afraid that I haven't managed to get this to work as of yet, however someone else might be able to have a go. The ideas was to use asynchronous I/O with the console std input (I assume the idea of your app is to allow people to write directly into the console window, and read the input as it comes).
I separated off all the API stuff into a module (MAsynchConsole):
Option Explicit
Private Const GENERIC_READ As Long = &H80000000
Private Const GENERIC_WRITE As Long = &H40000000
Private Const OPEN_EXISTING As Long = 3&
Private Const FILE_FLAG_OVERLAPPED As Long = &H40000000
Private Const FILE_SHARE_READ As Long = &H1
Private Const FILE_FLAG_NO_BUFFERING As Long = &H20000000
Private Type OVERLAPPED
Internal As Long
InternalHigh As Long
OffsetOrPointer As Long
OffsetHigh As Long
hEvent As Long
End Type
Private Type OVERLAPPED_ENTRY
lpCompletionKey As Long
lpOverlapped As Long ' pointer to OVERLAPPED
Internal As Long
dwNumberOfBytesTransferred As Long
End Type
Private Declare Function AllocConsole Lib "kernel32" () As Long
Private Declare Function CancelIo Lib "Kernel32.dll" ( _
ByVal hFile As Long _
) As Long
Private Declare Function CreateFile Lib "Kernel32.dll" Alias "CreateFileW" ( _
ByVal lpFileName As Long, _
ByVal dwDesiredAccess As Long, _
ByVal dwShareModen As Long, _
ByRef lpSecurityAttributes As Any, _
ByVal dwCreationDisposition As Long, _
ByVal dwFlagsAndAttributes As Long, _
ByVal hTemplateFile As Long _
) As Long
Private Declare Function FreeConsole Lib "kernel32" () As Long
Private Declare Function GetStdHandle Lib "kernel32" ( _
ByVal nStdHandle As Long _
) As Long
Private Declare Function ReadFile Lib "Kernel32.dll" ( _
ByVal hFile As Long, _
ByVal lpBuffer As Long, _
ByVal nNumberOfBytesToRead As Long, _
ByRef lpNumberOfBytesRead As Long, _
ByRef lpOverlapped As OVERLAPPED _
) As Long
Private Declare Function ReadFileEx Lib "Kernel32.dll" ( _
ByVal hFile As Long, _
ByVal lpBuffer As Long, _
ByVal nNumberOfBytesToRead As Long, _
ByRef lpOverlapped As OVERLAPPED, _
ByVal lpCompletionRoutine As Long _
) As Long
Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
Private m_hStdIn As Long
Private m_uOverlapped As OVERLAPPED
Private m_sUnicodeBuffer As String
Private m_oReadCallback As IReadCallback
Public Sub CloseConsole()
CancelIo m_hStdIn
Set m_oReadCallback = Nothing
m_sUnicodeBuffer = vbNullString
CloseHandle m_hStdIn
FreeConsole
End Sub
Private Sub FileIOCompletionRoutine( _
ByVal dwErrorCode As Long, _
ByVal dwNumberOfBytesTransfered As Long, _
ByRef uOverlapped As OVERLAPPED _
)
On Error GoTo ErrorHandler
m_oReadCallback.DataRead "FileIOCompletionRoutine"
m_oReadCallback.DataRead "dwErrorCode = " & CStr(dwErrorCode)
If dwErrorCode Then
MsgBox "Error = " & CStr(dwErrorCode)
CloseConsole
Exit Sub
End If
m_oReadCallback.DataRead "dwNumberOfBytesTransfered = " & CStr(dwNumberOfBytesTransfered)
m_oReadCallback.DataRead Left$(m_sUnicodeBuffer, dwNumberOfBytesTransfered)
Exit Sub
ErrorHandler:
'
End Sub
Public Sub OpenConsoleForInput(ByRef the_oReadCallback As IReadCallback)
Dim sFileName As String
On Error GoTo ErrorHandler
Set m_oReadCallback = the_oReadCallback
AllocConsole
'm_hStdIn = GetStdHandle(-10&)
sFileName = "CONIN$"
'm_hStdIn = CreateFile(StrPtr(sFileName), GENERIC_READ, FILE_SHARE_READ, ByVal 0&, OPEN_EXISTING, 0&, 0&)
m_hStdIn = CreateFile(StrPtr(sFileName), GENERIC_READ Or GENERIC_WRITE, FILE_SHARE_READ, ByVal 0&, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, 0&)
m_oReadCallback.DataRead "m_hStdIn = " & CStr(m_hStdIn)
m_oReadCallback.DataRead "LastError = " & CStr(Err.LastDllError)
m_sUnicodeBuffer = Space$(8192)
Exit Sub
ErrorHandler:
Err.Raise Err.Number, Err.Source, Err.Description
End Sub
Public Sub Read()
Dim nRet As Long
Dim nBytesRead As Long
On Error GoTo ErrorHandler
m_oReadCallback.DataRead "About to call ReadFileExe"
'm_uOverlapped.OffsetHigh = 0&
'm_uOverlapped.OffsetOrPointer = 0&
'nRet = ReadFile(m_hStdIn, StrPtr(m_sUnicodeBuffer), LenB(m_sUnicodeBuffer), nBytesRead, m_uOverlapped)
nRet = ReadFileEx(m_hStdIn, StrPtr(m_sUnicodeBuffer), LenB(m_sUnicodeBuffer), m_uOverlapped, AddressOf FileIOCompletionRoutine)
m_oReadCallback.DataRead "nRet = " & CStr(nRet)
m_oReadCallback.DataRead "nBytesRead = " & CStr(nBytesRead)
If nRet = 0 Then
m_oReadCallback.DataRead "Err.LastDllError = " & CStr(Err.LastDllError)
Else
m_oReadCallback.DataRead StrConv(Left$(m_sUnicodeBuffer, nBytesRead), vbUnicode)
End If
Exit Sub
ErrorHandler:
Err.Raise Err.Number, Err.Source, Err.Description
End Sub
This relies on an interface (IReadCallback) to communicate with the main GUI.
Option Explicit
Public Sub DataRead(ByRef out_sData As String)
'
End Sub
This is my sample form (FAsynchConsoleTest) - which uses a Timer (Timer) and RichTextBox (txtStdIn):
Option Explicit
Implements IReadCallback
Private Sub Form_Load()
MAsynchConsole.OpenConsoleForInput Me
Timer.Enabled = True
End Sub
Private Sub Form_Unload(Cancel As Integer)
MAsynchConsole.CloseConsole
End Sub
Private Sub IReadCallback_DataRead(out_sData As String)
txtStdIn.SelStart = Len(txtStdIn.Text)
txtStdIn.SelText = vbNewLine & out_sData
End Sub
Private Sub mnuTimerOff_Click()
Timer.Enabled = False
End Sub
Private Sub mnuTimerOn_Click()
Timer.Enabled = True
End Sub
Private Sub Timer_Timer()
MAsynchConsole.Read
End Sub
Unfortunately, whilst CreateFile() using FILE_FLAG_OVERLAPPED should create a file handle that can be used with async I/O, and the handle seems valid, ReadFileEx() returns 0, and GetLastError is 6, which is:
//
// MessageId: ERROR_INVALID_HANDLE
//
// MessageText:
//
// The handle is invalid.
//
#define ERROR_INVALID_HANDLE 6L
The console, interestingly, is frozen whilst this all happens.
Anyone else have any ideas? The docs seem to suggest that if you use CreateFile() with a console device name, the parameter is ignored.
wqw's answer doesn't work for a form-based application, but the prototypes given there for Peek/ReadConsoleInput allow for one that does:
Private Declare Function AllocConsole Lib "kernel32" () As Long
Private Declare Function FreeConsole Lib "kernel32" () As Long
Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
Private Declare Function GetStdHandle Lib "kernel32" (ByVal nStdHandle As Long) As Long
Private Declare Function PeekConsoleInput Lib "kernel32" Alias "PeekConsoleInputA" (ByVal hConsoleInput As Long, lpBuffer As Any, ByVal nLength As Long, lpNumberOfEventsRead As Long) As Long
Private Declare Function ReadConsoleInput Lib "kernel32" Alias "ReadConsoleInputA" (ByVal hConsoleInput As Long, lpBuffer As Any, ByVal nLength As Long, lpNumberOfEventsRead As Long) As Long
Private Declare Function ReadFile Lib "kernel32" (ByVal hFile As Long, lpBuffer As Any, ByVal nNumberOfBytesToRead As Long, lpNumberOfBytesRead As Long, ByVal lpOverlapped As Long) As Long
Private Declare Function SetConsoleMode Lib "kernel32" (ByVal hConsoleInput As Long, dwMode As Long) As Long
Private Const STD_INPUT_HANDLE As Long = -10& ' GetStdHandle()
Private Const KEY_EVENT As Long = 1 ' PeekConsoleInput()
Private Const ENABLE_PROCESSED_INPUT As Long = &H1 ' SetConsoleMode()
Private Const ENABLE_ECHO_INPUT As Long = &H4
Dim hStdIn As Long
Private Sub Form_Load()
AllocConsole
hStdIn = GetStdHandle(STD_INPUT_HANDLE)
SetConsoleMode hStdIn, ENABLE_PROCESSED_INPUT ' Or ENABLE_ECHO_INPUT ' uncomment to see the characters typed (for debugging)
Timer1.Enabled = True
Exit Sub
End Sub
Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
CloseHandle hStdIn
FreeConsole
End Sub
Private Sub Timer1_Timer()
Dim bytesRead As Long
Dim buffer As String
Dim baBuffer(0 To 512) As Byte
Dim lEvents As Long
PeekConsoleInput hStdIn, baBuffer(0), 1, lEvents
If lEvents > 0 Then
If baBuffer(0) = KEY_EVENT And baBuffer(4) <> 0 Then ' baBuffer(4) = INPUT_RECORD.bKeyDown
buffer = Space$(1)
Call ReadFile(hStdIn, ByVal buffer, Len(buffer), bytesRead, 0)
' buffer now contains one byte read from console
' Statements to process go here.
Else
Call ReadConsoleInput(hStdIn, baBuffer(0), 1, lEvents)
End If
End If
End Sub
PeekNamedPipe, GetConsoleMode and PeekConsoleInput will all return zero if your app isn't a true VB6 console app (though all that may be required is linking with the console subsystem, e.g., "C:\Program Files\Microsoft Visual Studio\vb98\LINK.EXE" /EDIT /SUBSYSTEM:CONSOLE MyApp.exe, I haven't tested it that far). They still work, however, at least Peek... does.
It is key that only one byte is read on each pass, as reading what is in baBuffer is problematic past the first record (INPUT_RECORD structure), but one byte at a time non-blocking is better than none at all. For me, Timer1 is set at 100 ms, but a better setting might be 55 ms, the events time slice.
Also key is that ReadConsoleInput is non-blocking if there is an event present on stdin, not just a key to be read. Using it when the recognized event isn't a key, effectively clears the event, allowing the application to proceed. It is possible to use this to read the bytes from the buffer without using ReadFile at all:
PeekConsoleInput hStdIn, baBuffer(0), 1, lEvents
If lEvents > 0 Then
Call ReadConsoleInput(hStdIn, baBuffer(0), 1, lEvents)
If baBuffer(0) = KEY_EVENT And baBuffer(4) <> 0 Then
' Chr(baBuffer(14)) now produces the character typed...
This hasn't been tested for reading true human input, except in the simplest debugging during construction, but it does work and should allow most VB6 form-based apps to effectively use a console. Thank you wqw!
I am using GetTokenInformation as a part of the code that determines if the current thread is running as an Administrator.
Anyway, I have a structure for TOKEN INFORMATION that looks like this:
Private Type TOKEN_GROUPS
GroupCount As Long
Groups(500) As SID_AND_ATTRIBUTES
End Type
Then, I invoke GetTokenInformation like so:
res = GetTokenInformation(<Process Handle>, 2, <TOKEN_GROUPS>, _
<Token Info Length>, <Buffer Length)
The first invocation is to get the Buffer Length, then I invoke it again to get the token information.
Anyway, the application will suddenly crash when the account that run the application is connected to a domain. Apparently, the size of the,
Groups(500) As SID.AND.ATTRIBUTES
is not enough and is causing a buffer overrun. I don't know why that is (MSDN says that I should provide an ANYSIZE_ARRAY or 1). Increasing the size of the Groups to 1000 fixes the problem.
As a quick fix and since I don't have an idea on how to get the appropriate size of Groups, I am planning to just ReDim the Groups until the call succeeds.
Here's my question:
I have an On Error clause, but when the buffer overrun occurs, the On Error can't catch it and my app suddenly crashes. Why is that?
Given the code below
Private Type TOKEN_GROUPS
GroupCount As Long
Groups() As SID_AND_ATTRIBUTES 'FAILING
'Groups(1000) As SID_AND_ATTRIBUTES DOES NOT FAIL
End Type
Dim X as TOKEN_GROUPS
ReDim Preserve X.Groups(1000) As SID_AND_ATTRIBUTES 'FAILING
res = GetTokenInformation(<Process Handle>, 2, <TOKEN_GROUPS>, <Token Info Length>, <Buffer Length)
res = GetTokenInformation(<Process Handle>, 2, <TOKEN_GROUPS>, <Token Info Length>, <Buffer Length)
Why is that when I declared Groups as 1000, the GetTokenInformation call is not failing but when I declared an "empty" Groups() and ReDim'd it to 1000, it is failing?
If you want to use dynamicly sized array for Groups you'll need "custom API call marshaling" code. Basicly couple of CopyMemory's and an array resize
Option Explicit
'--- for OpenProcessToken
Private Const TOKEN_READ As Long = &H20008
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (pTo As Any, uFrom As Any, ByVal lSize As Long)
Private Declare Function GetCurrentProcess Lib "kernel32" () As Long
Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
Private Declare Function OpenProcessToken Lib "advapi32.dll" (ByVal ProcessHandle As Long, ByVal DesiredAccess As Long, TokenHandle As Long) As Long
Private Declare Function GetTokenInformation Lib "advapi32.dll" (ByVal TokenHandle As Long, ByVal TokenInformationClass As Long, TokenInformation As Any, ByVal TokenInformationLength As Long, ReturnLength As Long) As Long
Private Type SID_AND_ATTRIBUTES
Sid As Long
Attributes As Long
End Type
Private Type VB_TOKEN_GROUPS
GroupCount As Long
Groups() As SID_AND_ATTRIBUTES
End Type
Private Sub Command1_Click()
Dim hProcessID As Long
Dim hToken As Long
Dim lNeeded As Long
Dim baBuffer() As Byte
Dim uGroups As VB_TOKEN_GROUPS
hProcessID = GetCurrentProcess()
If hProcessID <> 0 Then
If OpenProcessToken(hProcessID, TOKEN_READ, hToken) = 1 Then
Call GetTokenInformation(hToken, 2, ByVal 0, 0, lNeeded)
ReDim baBuffer(0 To lNeeded)
'--- enum TokenInformationClass { TokenUser = 1, TokenGroups = 2, ... }
If GetTokenInformation(hToken, 2, baBuffer(0), UBound(baBuffer), lNeeded) = 1 Then
Call CopyMemory(uGroups.GroupCount, baBuffer(0), 4)
ReDim uGroups.Groups(0 To uGroups.GroupCount - 1)
Call CopyMemory(uGroups.Groups(0), baBuffer(4), uGroups.GroupCount * Len(uGroups.Groups(0)))
End If
Call CloseHandle(hToken)
End If
Call CloseHandle(hProcessID)
End If
End Sub
There is another question here that seems to have solved the GetTokenInformation call.
Copied from the accepted answer:
Call GetTokenInformation(hToken, 1, ByVal 0, 0, lNeeded)
ReDim baBuffer(0 To lNeeded)
...