I have an Excel file containing metadata information for 20k+ images. I'm trying to write a macro that executes commands with exiftool.exe (a tool used to batch edit metadata) on Windows Shell with variables relative to each row.
For instance, I want to iterate through exiftool commands that take information from column B ("Author") such as:
C:\exiftool\exiftool.exe -Author="CELL B1 CONTENT"
C:\exiftool\exiftool.exe -Author="CELL B2 CONTENT"
...repeats ad infinitum.
This is what I've tried so far:
Sub EnterMetadata()
For Each Cell In Range("C1:C20000")
Shell("c:\Exiftool\exiftool.exe -o I:/Photos/ & ActiveCell.Offset(0, -2).Value) & " -Author=" & ActiveCell.Offset(0, -1).Value)
Next
End Sub
Notice that column A contains the path for the original files. And column B contains author names. I'm trying to retrieve information from columns A and B to use in the macro.
Untested:
Sub EnterMetadata()
Const CMD As String = "c:\Exiftool\exiftool.exe -o ""I:/Photos/{fn}"" -Author=""{auth}"""
Dim Cell as Range, s as String
For Each Cell In Range("C1:C20000")
s = Replace(CMD, "{fn}", Cell.Offset(0, -2).Value)
s = Replace(s, "{auth}", Cell.Offset(0, -1).Value)
Debug.Print s
Shell s
Next
End Sub
If any of your command line parameters might contain spaces then you should quote them (quotes are escaped in VBA strings by doubling them up)
What about using ShellExecute?
This is what you need to declare in your macro so you can use it:
Private Declare Function ShellExecute Lib "shell32.dll" Alias "ShellExecuteA" ( _
ByVal hWnd As Long, _
ByVal lpOperation As String, _
ByVal lpFile As String, _
ByVal lpParameters As String, _
ByVal lpDirectory As String, _
ByVal nShowCmd As Long) As Long
You can pass parameters to it as String (lpParameters), get a return value tohelp understand what happened if something went wrong and is generally more versatile than using Shell.
You can do something similar to this (this is not tested, as I don't have exiftool.):
ShellExecute 0, vbNullString, "C:\exiftool\exiftool.exe", "-Author=""CELL B1 CONTENT""", vbNullString, 10 ' 10=SW_SHOWDEFAULT
I'll let you populate the author according to your requirements.
For full information about ShellExecute, click here to have a look on MSDN.
Related
I am working in Access 2013 and try to get GetRawInputDeviceList, GetRawInputDeviceInfo, RegisterRawInputDevices and GetRawInputData equivalents for VBA with no success. I have also searched in vain for a procedure, function or module to get a list of connected HID devices to a computer to pick out a barcode scanner. This is the beginning of the third week so I am on my knees begging for assistance. Do any of you all have a module you're willing to share, a link to a website where this is dealt with? Any help is greatly appreciated.
Using the GetRawInputDeviceList API from VBA would be pretty tricky because of the pRawInputDeviceList parameter. Unless you're willing to jump through a ton of hoops to manage your own memory and manually handle the resulting array of RAWINPUTDEVICELIST in raw memory, you'll be better off coming at this from another direction.
Most barcode scanners I've dealt with present themselves to Windows as a keyboard. One possible solution would be to use a WMI query to enumerate attached Win32_Keyboard devices:
Private Sub ShowKeyboardInfo()
Dim WmiServer As Object
Dim ResultSet As Object
Dim Keyboard As Object
Dim Query As String
Query = "SELECT * From Win32_Keyboard"
Set WmiServer = GetObject("winmgmts:root/CIMV2")
Set ResultSet = WmiServer.ExecQuery(Query)
For Each Keyboard In ResultSet
Debug.Print Keyboard.Name & vbTab & _
Keyboard.Description & vbTab & _
Keyboard.DeviceID & vbTab & _
Keyboard.Status
Next Keyboard
End Sub
Note: If it doesn't turn up there, you can enumerate all of the USB devices by querying CIM_USBDevice: Query = "SELECT * From Win32_Keyboard"
EDIT: Per the comments, the above code won't return the handle needed to register to receive raw input events. This should get you started though - the RegisterRawInputDevices and GetRawInputData aspects are beyond the scope of what will easily go in an answer. Take a hack at it, and if you run into any problems post your code in another question.
Declarations:
Private Type RawInputDeviceList
hDevice As Long
dwType As Long
End Type
Private Type RidKeyboardInfo
cbSize As Long
dwType As Long
dwKeyboardMode As Long
dwNumberOfFunctionKeys As Long
dwNumberOfIndicators As Long
dwNumberOfKeysTotal As Long
End Type
Private Enum DeviceType
TypeMouse = 0
TypeKeyboard = 1
TypeHID = 2
End Enum
Private Enum DeviceCommand
DeviceName = &H20000007
DeviceInfo = &H2000000B
PreParseData = &H20000005
End Enum
Private Declare Function GetRawInputDeviceList Lib "user32" ( _
ByVal pRawInputDeviceList As Long, _
ByRef puiNumDevices As Long, _
ByVal cbSize As Long) As Long
Private Declare Function GetRawInputDeviceInfo Lib "user32" Alias "GetRawInputDeviceInfoW" ( _
ByVal hDevice As Long, _
ByVal uiCommand As Long, _
ByVal pData As Long, _
ByRef pcbSize As Long) As Long
Private Declare Function GetLastError Lib "kernel32" () As Long
Sample of retrieving device names with GetRawInputDeviceInfo:
Private Sub SampleCode()
Dim devices() As RawInputDeviceList
devices = GetRawInputDevices
Dim i As Long
For i = 0 To UBound(devices)
'Inspect the type - only looking for a keyboard.
If devices(i).dwType = TypeKeyboard Then
Dim buffer As String
Dim size As Long
'First call with a null pointer returns the string length in size.
If GetRawInputDeviceInfo(devices(i).hDevice, DeviceName, 0&, size) = -1 Then
Debug.Print "GetRawInputDeviceInfo error " & GetLastError()
Else
'Size the string buffer.
buffer = String(size, Chr$(0))
'The second call copies the name into the passed buffer.
If GetRawInputDeviceInfo(devices(i).hDevice, DeviceName, StrPtr(buffer), size) = -1 Then
Debug.Print "GetRawInputDeviceInfo error " & GetLastError()
Else
Debug.Print buffer
End If
End If
End If
Next i
End Sub
Private Function GetRawInputDevices() As RawInputDeviceList()
Dim devs As Long
Dim output() As RawInputDeviceList
'First call with a null pointer returns the number of devices in devs
If GetRawInputDeviceList(0&, devs, LenB(output(0))) = -1 Then
Debug.Print "GetRawInputDeviceList error " & GetLastError()
Else
'Size the output array.
ReDim output(devs - 1)
'Second call actually fills the array.
If GetRawInputDeviceList(VarPtr(output(0)), devs, LenB(output(0))) = -1 Then
Debug.Print "GetRawInputDeviceList error " & GetLastError()
Else
GetRawInputDevices = output
End If
End If
End Function
Sorry about the side scrolling.
I am using below code to automate saving the PDF document from Internet Explorer window. It's working fine, but I want it to be happened for multiple PDF files with multiple URL's.
When I give URL's in column A and destination path with file format as .pdf in column B by taking URL from column A and save file with file name from column B.
Option Explicit
Private Declare Function URLDownloadToFile Lib "urlmon" _
Alias "URLDownloadToFileA" ( _
ByVal pCaller As Long, _
ByVal szURL As String, _
ByVal szFileName As String, _
ByVal dwReserved As Long, _
ByVal lpfnCB As Long _
) As Long
Sub z()
Dim strSource As String
Dim strDest As String
strSource = "http://www.cran.r-project.org/doc/manuals/R-intro.pdf"
strDest = "c:\temp\blah.pdf"
URLDownloadToFile 0, strSource, strDest, 0, 0
End Sub
As #shA.t was saying, all you need to do is to wrap those lines in a For Each loop.
Solution:
Let's say, you have your URLs in A1 to A3 and your destinations in B1 to B3. Your Sub z() should look like this:
For Each source in Sheets("Sheet name").Range("A1:A3")
URLDownloadToFile 0, source.Value, source.Offset(0,1).Value, 0, 0
Next source
Explanation: For Each loops through all cell elements in range A1 to A3. In each round of the loop, source becomes that cell. Instead of hardcoding the source into your code, you can just refer to source.Value, the contents of the current cell in the loop. For the destination, you can use the .Offset method which references to a neighboring cell by its relative distance to the cell it is being called from. In this case, we want to get from A1 to B1 (and so on), i.e. zero rows down, one column right (Offset(0,1)).
Good afternoon folks -
I'm working on reading/writing an external file that is created and managed by a 3rd party that uses .INI structured files as its scripting language. I've got a wrapper working pretty well however, the section names are static with a unique number at the end ([GENERAL-1]) so that you have have the same task more than once. I am using VB.NET w/ VS2008.
My code below can successfully read a key from a section that is hardcoded but I'd like the key to be generic.
INI
test.ini
[GENERAL-1]
SUPPRESSTASKERRORS=Y
TASKERRORSENDJOB=Y
Code:
Declare Function GetPrivateProfileString Lib "kernel32.dll" Alias
"GetPrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As
String, ByVal lpDefault As String, ByVal lpReturnedString As String, ByVal nSize As
Long, ByVal lpFileName As String) As Long
Declare Function WriteProfileString Lib "kernel32.dll" Alias
"WritePrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As
String, ByVal lpString As String, ByVal lpFileName As String) As Long
' Read INI file
Dim uname As String ' receives the value read from the INI file
Dim slength As Long ' receives length of the returned string
Dim OriginalMJB As String = "c:\test\test.ini"
uname = Space(1024)
slength = GetPrivateProfileString("General-1", "SUPPRESSTASKERRORS", "anonymous",
uname, 1024, OriginalMJB)
Notice the General-1 above, if I have the value hardcoded as -1 I can read the input .ini file without a problem. Any thoughts on how I can get and use the value left of the hyphen?
Any help is appreciated!
--George
Here's one way. From here you should be able to make SectionNo equal the specific section you want.
Dim section As String = "General"
Dim SectionNo as String = "-"
Dim Number as Integer = 1
SectionNo += Number.ToString
slength = GetPrivateProfileString(section + SectionNo, "SUPPRESSTASKERRORS", "anonymous", uname, 1024, OriginalMJB)
Here's a couple of options
Dim SectionName As String = "General-1"
Dim SectionCategorie As String = ""
Dim Section As String = ""
'Using Split - It returns an array so you can load the results into an array
'or just call it and load the specific index each time.
SectionCategorie = Split(SectionName, "-")(0)
Section = Split(SectionName, "-")(1)
'Using Substring
SectionCategorie = SectionName.Substring(0, SectionName.IndexOf("-"))
Section = SectionName.Substring(SectionName.IndexOf("-") + 1)
I am working on a program in Visual Basic 2008, I am required to have different types of sounds with varying volumes. Hence My.Computer.Audio.Play is not a valid option.
I decided to use mciSendString instead and found the following code
Private Declare Function mciSendString Lib "winmm.dll" Alias "mciSendStringA" _
(ByVal lpstrCommand As String, ByVal lpstrReturnString As String, _
ByVal uReturnLength As Integer, ByVal hwndCallback As Integer) As Integer
mciSendString("close myWAV", Nothing, 0, 0)
Dim fileName1 As String =
mciSendString("open " & fileName1 & " type mpegvideo alias myWAV", Nothing, 0, 0)
mciSendString("play myWAV", Nothing, 0, 0)
'min Volume is 1, max Volume is 1000
Dim Volume As Integer = (SFXVolume * 100)
mciSendString("setaudio myWAV volume to " & Volume, Nothing, 0, 0)
Now this code I have tested and is working perfectly when filename1 = "C://Correct.wav"
However when I use
filename1 = My.Application.Info.DirectoryPath & "\Correct.wav"
I get no sound play whatsoever.
Could anyone please help me correct my code so that this works.
Thank you in advance.
If your DirectoryPath has spaces then mciSendString won't be able to recognize the command accurately, you need to surround the path with quotes:
mciSendString(
String.Format("open ""{0}"" type mpegvideo alias myWAV", fileName1), Nothing, 0, 0)
Be sure to check returned status as well, as Hans suggests.
Also, since you don't know whether DirectoryPath has a trailing backslash or not, the accurate way to produce full path from directory and name is:
fileName1 = System.IO.Path.Combine(My.Application.Info.DirectoryPath, "Correct.wav")
Use Private Declare Function SetCurrentDirectory Lib "kernel32" Alias "SetCurrentDirectoryA" (ByVal lpPathName As String) As Long then SetCurrentDirectory filepath before opening file for play. That is working for me.
You need to use the DLL Call GetShortPathName in order to pass file paths to WINMM.DLL.
lpszLongPath is your full path string, and the short pathname will be passed to lpszShortPath.
cchbuffer should really be set to 200 or so, though in most cases, the returned string will be much shorter. You should use a VB padded string.
Private Declare Function GetShortPathName Lib "kernel32" Alias "GetShortPathNameA" (ByVal lpszLongPath As String, ByVal lpszShortPath As String, ByVal cchBuffer As Long) As Long
I have just used the mciSendString calls in a batch midi-file reading programme, opening 3642 midi files and returning copyright, title and play duration strings actually quite quickly!
Best Regards
David R Leach
I want to get the full name of the user (logged in already) in VBA. This code I found online would do getting the username:
UserName = Environ("USERNAME")
but I want the user's real name. I found some hint about NetUserGetInfo but not sure what to think or do. Any hints will be appreciated
Regards,
Even if this thread is rather old, other users might be still googling around (like me).
I found an excellent short solution that worked for me out-of-the-box (thanks to Mr.Excel.com).
I changed it because I needed it to return a string with the user's full name.
The original post is here.
EDIT:
Well, I fixed a mistake, "End Sub" instead of "End Function" and added a variable declaration statement, just in case. I tested it in Excel 2010 and 2013 versions. Worked fine on my home pc too (no domain, just in a workgroup).
' This function returns the full name of the currently logged-in user
Function GetUserFullName() as String
Dim WSHnet, UserName, UserDomain, objUser
Set WSHnet = CreateObject("WScript.Network")
UserName = WSHnet.UserName
UserDomain = WSHnet.UserDomain
Set objUser = GetObject("WinNT://" & UserDomain & "/" & UserName & ",user")
GetUserFullName = objUser.FullName
End Function
I found the API answer complex as well in addition to needing recoding from a form to module
The function below comes courtesy of Rob Sampson from this Experts-Exchange post. It is a flexible function, see code comments for details. Please note it was a vbscript so the variables are not dimensioned
Sub Test()
strUser = InputBox("Please enter a username:")
struserdn = Get_LDAP_User_Properties("user", "samAccountName", strUser, "displayName")
If Len(struserdn) <> 0 Then
MsgBox struserdn
Else
MsgBox "No record of " & strUser
End If
End Sub
Function Get_LDAP_User_Properties(strObjectType, strSearchField, strObjectToGet, strCommaDelimProps)
' This is a custom function that connects to the Active Directory, and returns the specific
' Active Directory attribute value, of a specific Object.
' strObjectType: usually "User" or "Computer"
' strSearchField: the field by which to seach the AD by. This acts like an SQL Query's WHERE clause.
' It filters the results by the value of strObjectToGet
' strObjectToGet: the value by which the results are filtered by, according the strSearchField.
' For example, if you are searching based on the user account name, strSearchField
' would be "samAccountName", and strObjectToGet would be that speicific account name,
' such as "jsmith". This equates to "WHERE 'samAccountName' = 'jsmith'"
' strCommaDelimProps: the field from the object to actually return. For example, if you wanted
' the home folder path, as defined by the AD, for a specific user, this would be
' "homeDirectory". If you want to return the ADsPath so that you can bind to that
' user and get your own parameters from them, then use "ADsPath" as a return string,
' then bind to the user: Set objUser = GetObject("LDAP://" & strReturnADsPath)
' Now we're checking if the user account passed may have a domain already specified,
' in which case we connect to that domain in AD, instead of the default one.
If InStr(strObjectToGet, "\") > 0 Then
arrGroupBits = Split(strObjectToGet, "\")
strDC = arrGroupBits(0)
strDNSDomain = strDC & "/" & "DC=" & Replace(Mid(strDC, InStr(strDC, ".") + 1), ".", ",DC=")
strObjectToGet = arrGroupBits(1)
Else
' Otherwise we just connect to the default domain
Set objRootDSE = GetObject("LDAP://RootDSE")
strDNSDomain = objRootDSE.Get("defaultNamingContext")
End If
strBase = "<LDAP://" & strDNSDomain & ">"
' Setup ADO objects.
Set adoCommand = CreateObject("ADODB.Command")
Set ADOConnection = CreateObject("ADODB.Connection")
ADOConnection.Provider = "ADsDSOObject"
ADOConnection.Open "Active Directory Provider"
adoCommand.ActiveConnection = ADOConnection
' Filter on user objects.
'strFilter = "(&(objectCategory=person)(objectClass=user))"
strFilter = "(&(objectClass=" & strObjectType & ")(" & strSearchField & "=" & strObjectToGet & "))"
' Comma delimited list of attribute values to retrieve.
strAttributes = strCommaDelimProps
arrProperties = Split(strCommaDelimProps, ",")
' Construct the LDAP syntax query.
strQuery = strBase & ";" & strFilter & ";" & strAttributes & ";subtree"
adoCommand.CommandText = strQuery
' Define the maximum records to return
adoCommand.Properties("Page Size") = 100
adoCommand.Properties("Timeout") = 30
adoCommand.Properties("Cache Results") = False
' Run the query.
Set adoRecordset = adoCommand.Execute
' Enumerate the resulting recordset.
strReturnVal = ""
Do Until adoRecordset.EOF
' Retrieve values and display.
For intCount = LBound(arrProperties) To UBound(arrProperties)
If strReturnVal = "" Then
strReturnVal = adoRecordset.Fields(intCount).Value
Else
strReturnVal = strReturnVal & vbCrLf & adoRecordset.Fields(intCount).Value
End If
Next
' Move to the next record in the recordset.
adoRecordset.MoveNext
Loop
' Clean up.
adoRecordset.Close
ADOConnection.Close
Get_LDAP_User_Properties = strReturnVal
End Function
This works for me. It might need some adjustments - I get several items returned and only one has .Flags > 0
Function GetUserFullName() As String
Dim objWin32NLP As Object
On Error Resume Next
' Win32_NetworkLoginProfile class https://msdn.microsoft.com/en-us/library/aa394221%28v=vs.85%29.aspx
Set objWin32NLP = GetObject("WinMgmts:").InstancesOf("Win32_NetworkLoginProfile")
If Err.Number <> 0 Then
MsgBox "WMI is not installed", vbExclamation, "Windows Management Instrumentation"
Exit Function
End If
For Each objItem In objWin32NLP
If objItem.Flags > 0 Then GetUserFullName = objItem.FullName
Next
End Function
Try this:
How To Call NetUserGetInfo from Visual Basic
(From Microsoft Knowledge Base, article ID 151774)
The NetUserGetInfo function is a Unicode-only Windows NT API. The last parameter of this function is a pointer to a pointer to a structure whose members contain DWORD data and pointers to Unicode strings. In order to call this function correctly from a Visual Basic application, you need to de-reference the pointer returned by the function and then you need to convert the Visual Basic string to a Unicode string and vice versa. This article illustrates these techniques in an example that calls NetUserGetInfo to retrieve a USER_INFO_3 structure from a Visual Basic application.
The example below uses the Win32 RtlMoveMemory function to de-reference the pointer returned by the NetUserGetInfo call.
Step-by-Step Example
Start Visual Basic. If Visual Basic is already running, from the File menu, choose New Project. Form1 is created by default.
Add a Command button, Command1, to Form1.
Add the following code to the General Declarations section of Form1:
' definitions not specifically declared in the article:
' the servername and username params can also be declared as Longs,
' and passed Unicode memory addresses with the StrPtr function.
Private Declare Function NetUserGetInfo Lib "netapi32" _
(ByVal servername As String, _
ByVal username As String, _
ByVal level As Long, _
bufptr As Long) As Long
Const NERR_Success = 0
Private Declare Sub MoveMemory Lib "kernel32" Alias _
"RtlMoveMemory" (pDest As Any, pSource As Any, ByVal dwLength As Long)
Private Declare Function lstrlenW Lib "kernel32" (lpString As Any) As Long
' Converts a Unicode string to an ANSI string
' Specify -1 for cchWideChar and 0 for cchMultiByte to return string length.
Private Declare Function WideCharToMultiByte Lib "kernel32" _
(ByVal codepage As Long, _
ByVal dwFlags As Long, _
lpWideCharStr As Any, _
ByVal cchWideChar As Long, _
lpMultiByteStr As Any, _
ByVal cchMultiByte As Long, _
ByVal lpDefaultChar As String, _
ByVal lpUsedDefaultChar As Long) As Long
Private Declare Function NetApiBufferFree Lib "netapi32" _
(ByVal Buffer As Long) As Long
' CodePage
Const CP_ACP = 0 ' ANSI code page
Private Type USER_INFO_3
usri3_name As Long 'LPWSTR in SDK
usri3_password As Long 'LPWSTR in SDK
usri3_password_age As Long 'DWORD in SDK
usri3_priv As Long 'DWORD in SDK
usri3_home_dir As Long 'LPWSTR in SDK
usri3_comment As Long 'LPWSTR in SDK
usri3_flags As Long 'DWORD in SDK
usri3_script_path As Long 'LPWSTR in SDK
usri3_auth_flags As Long 'DWORD in SDK
usri3_full_name As Long 'LPWSTR in SDK
usri3_usr_comment As Long 'LPWSTR in SDK
usri3_parms As Long 'LPWSTR in SDK
usri3_workstations As Long 'LPWSTR in SDK
usri3_last_logon As Long 'DWORD in SDK
usri3_last_logoff As Long 'DWORD in SDK
usri3_acct_expires As Long 'DWORD in SDK
usri3_max_storage As Long 'DWORD in SDK
usri3_units_per_week As Long 'DWORD in SDK
usri3_logon_hours As Long 'PBYTE in SDK
usri3_bad_pw_count As Long 'DWORD in SDK
usri3_num_logons As Long 'DWORD in SDK
usri3_logon_server As Long 'LPWSTR in SDK
usri3_country_code As Long 'DWORD in SDK
usri3_code_page As Long 'DWORD in SDK
usri3_user_id As Long 'DWORD in SDK
usri3_primary_group_id As Long 'DWORD in SDK
usri3_profile As Long 'LPWSTR in SDK
usri3_home_dir_drive As Long 'LPWSTR in SDK
usri3_password_expired As Long 'DWORD in SDK
End Type
Private Sub Command1_Click()
Dim lpBuf As Long
Dim ui3 As USER_INFO_3
' Replace "Administrator" with a valid Windows NT user name.
If (NetUserGetInfo("", StrConv("Administrator", vbUnicode), 3, _
uf) = NERR_Success) Then
Call MoveMemory(ui3, ByVal lpBuf, Len(ui3))
MsgBox GetStrFromPtrW(ui3.usri3_name)
Call NetApiBufferFree(ByVal lpBuf)
End If
End Sub
' Returns an ANSI string from a pointer to a Unicode string.
Public Function GetStrFromPtrW(lpszW As Long) As String
Dim sRtn As String
sRtn = String$(lstrlenW(ByVal lpszW) * 2, 0) ' 2 bytes/char
' WideCharToMultiByte also returns Unicode string length
' sRtn = String$(WideCharToMultiByte(CP_ACP, 0, ByVal lpszW, -1, 0, 0, 0, 0), 0)
Call WideCharToMultiByte(CP_ACP, 0, ByVal lpszW, -1, ByVal sRtn, Len(sRtn), 0, 0)
GetStrFromPtrW = GetStrFromBufferA(sRtn)
End Function
' Returns the string before first null char encountered (if any) from an ANSI string.
Public Function GetStrFromBufferA(sz As String) As String
If InStr(sz, vbNullChar) Then
GetStrFromBufferA = Left$(sz, InStr(sz, vbNullChar) - 1)
Else
' If sz had no null char, the Left$ function
' above would return a zero length string ("").
GetStrFromBufferA = sz
End If
End Function
I would recommend re-factoring this into a module rather than embedding it in the form itself. I've used this successfully in Access in the past.
I've tried so many things, but I suppose my organization does not allow me to query Active Directory (or I got the structure wrong). I could only get my account name (not full name) or the error "No mapping between account names and security IDs was done"
But after 2 weeks searching, I finally have a working solution that I wanted to share. My final hint can be found here: https://www.mrexcel.com/board/threads/application-username-equivalent-in-ms-access.1143798/page-2#post-5545265
The value does appear in the registry i.e.
"HKEY_CURRENT_USER\Software\Microsoft\Office\Common\UserInfo\UserName"
Once I realized that, it was easy to access with VBA:
UserName = CreateObject("wscript.shell").RegRead("HKEY_CURRENT_USER\Software\Microsoft\Office\Common\UserInfo\UserName")
I assume (did not test though) that this is what Application.Username from Excel uses as well. Might not be perfect, but I finally have a solution that works.