How can I track users of my Excel worksheet? - excel

I've created an Excel worksheet and I would like to track who in my company uses it. Currently, it's freely available on our company intranet for downloading without any restriction.
I would like to implement a restriction where the Excel worksheet's VBA functionality stops working after 12 months of use. The user would have to contact me for an "reactivation code" of some sort to let the user continue using the sheet for another 12 months.
If the user doesn't find the Excel worksheet useful then they simply don't need a reactivation code. Is this possible to do within Excel?
EDIT 1: I need to stay within the confines of Excel. I don't want to bring in other options like embedding with an .exe or creating restrictions on the downloading of the Excel file on the company website. Thanks.

I have run into a similar situation previously.
If you expect that your users are going to be online when they use the application, you can make a simple http request from within a sub that's called when the worksheet is opened; that request can include the user name, and your server can log the request (and thus know who is using the application). To make it less inconvenient for the users, make sure that you include some failsafe code so that the application works normally when the server cannot be reached / is down.
You need to know how to do five things:
Run code when the worksheet is opened
Request the user (network) name to insert in the request
Make an http request from inside VBA (handle differences between PC and Mac...)
Handle failure of the request gracefully (don't cripple the worksheet)
Log the request so you have information about the use
Let me know if you don't know how to do one of these, and I can help further (but there will be a bit of delay in my response...). Answers for all these can be found on SO, but the synthesis may take some effort.
solution
Warning - this is a bit of a monster piece of code. I wrote it as much for myself as for you... It may need further explanation.
step 1 Add this code to ThisWorkbook in order to respond to the file being opened:
Private Sub Workbook_Open()
On Error GoTo exitSub
registerUse
exitSub:
End Sub
This calls the registerUse Sub when the workbook is opened.
step 2 get the user name. This is quite complex; create a module called "username" and paste in all the following code (note - a chunk of this was copied from Dev Ashish, the rest - in particular, dealing with the Mac solution - is my own work). Call the function currentUserName() to get the current user name (if it can resolve the "long name" from the network, it will; otherwise it will use the name/ID you use to log in with):
' ******** Code Start ********
'This code was originally written by Dev Ashish.
'It is not to be altered or distributed,
'except as part of an application.
'You are free to use it in any application,
'provided the copyright notice is left unchanged.
'
'Code Courtesy of
'Dev Ashish
'
' Modifications by Floris - mostly to make Mac compatible
Private Type USER_INFO_2
usri2_name As Long
usri2_password As Long ' Null, only settable
usri2_password_age As Long
usri2_priv As Long
usri2_home_dir As Long
usri2_comment As Long
usri2_flags As Long
usri2_script_path As Long
usri2_auth_flags As Long
usri2_full_name As Long
usri2_usr_comment As Long
usri2_parms As Long
usri2_workstations As Long
usri2_last_logon As Long
usri2_last_logoff As Long
usri2_acct_expires As Long
usri2_max_storage As Long
usri2_units_per_week As Long
usri2_logon_hours As Long
usri2_bad_pw_count As Long
usri2_num_logons As Long
usri2_logon_server As Long
usri2_country_code As Long
usri2_code_page As Long
End Type
Private Declare Function apiNetGetDCName _
Lib "netapi32.dll" Alias "NetGetDCName" _
(ByVal servername As Long, _
ByVal DomainName As Long, _
bufptr As Long) As Long
' function frees the memory that the NetApiBufferAllocate
' function allocates.
Private Declare Function apiNetAPIBufferFree _
Lib "netapi32.dll" Alias "NetApiBufferFree" _
(ByVal buffer As Long) _
As Long
' Retrieves the length of the specified wide string.
Private Declare Function apilstrlenW _
Lib "kernel32" Alias "lstrlenW" _
(ByVal lpString As Long) _
As Long
Private Declare Function apiNetUserGetInfo _
Lib "netapi32.dll" Alias "NetUserGetInfo" _
(servername As Any, _
username As Any, _
ByVal level As Long, _
bufptr As Long) As Long
' moves memory either forward or backward, aligned or unaligned,
' in 4-byte blocks, followed by any remaining bytes
Private Declare Sub sapiCopyMem _
Lib "kernel32" Alias "RtlMoveMemory" _
(Destination As Any, _
Source As Any, _
ByVal Length As Long)
Private Declare Function apiGetUserName Lib _
"advapi32.dll" Alias "GetUserNameA" _
(ByVal lpBuffer As String, _
nSize As Long) _
As Long
Private Const MAXCOMMENTSZ = 256
Private Const NERR_SUCCESS = 0
Private Const ERROR_MORE_DATA = 234&
Private Const MAX_CHUNK = 25
Private Const ERROR_SUCCESS = 0&
Function currentUserID()
' added this function to isolate user from windows / mac differences
' hoping this works!
' note - one can also use Application.OperatingSystem like "*Mac*" etc.
Dim tempString
On Error GoTo CUIerror
tempString = "Unknown"
#If Win32 Or Win64 Then
tempString = fGetUserName
#Else
tempString = whoIsThisMacID
#End If
' trim string to correct length ... there's some weirdness in the returned value
' we fall to this point if there's an error in the lower level functions, too
' in that case we will have the default value "Unknown"
CUIerror:
currentUserID = Left(tempString, Len(tempString))
End Function
Function currentUserName()
Dim tempString
On Error GoTo CUNerror
tempString = "Unknown"
#If Win32 Or Win64 Then
tempString = fGetFullNameOfLoggedUser
#Else
tempString = whoIsThisMacName
#End If
' trim string to get rid of weirdness at the end...
' and fall through on error:
CUNerror:
currentUserName = Left(tempString, Len(tempString))
' in some cases the lower level functions return a null string:
If Len(currentUserName) = 0 Then currentUserName = currentUserID
End Function
#If Mac Then
Function whoIsThisMacID()
Dim sPath As String, sCmd As String
On Error GoTo WIDerror
sPath = "/usr/bin/whoami"
sCmd = "set RetVal1 to do shell script """ & sPath & """"
whoIsThisMacID = MacScript(sCmd)
Exit Function
WIDerror:
whoIsThisMacID = "unknown"
End Function
Function whoIsThisMacName()
' given the user ID, find the user name using some magic finger commands...
Dim cmdString As String
Dim sCmd As String
On Error GoTo WHOerror
' use finger command to find out more information about the current user
' use grep to strip the line with the Name: tag
' use sed to strip out string up to and including 'Name: "
' the rest of the string is the user name
cmdString = "/usr/bin/finger " & whoIsThisMacID & " | /usr/bin/grep 'Name:' | /usr/bin/sed 's/.*Name: //'"
' send the command to be processed by AppleScript:
sCmd = "set RetVal1 to do shell script """ & cmdString & """"
whoIsThisMacName = MacScript(sCmd)
Exit Function
WHOerror:
whoIsThisMacName = "unknown"
End Function
Sub testName()
MsgBox whoIsThisMacName
End Sub
#End If
' do not compile this code if it's not a windows machine... it's not going to work!
#If Win32 Or Win64 Then
Function fGetFullNameOfLoggedUser(Optional strUserName As String) As String
'
' Returns the full name for a given UserID
' NT/2000 only
' Omitting the strUserName argument will try and
' retrieve the full name for the currently logged on user
'
On Error GoTo ErrHandler
Dim pBuf As Long
Dim dwRec As Long
Dim pTmp As USER_INFO_2
Dim abytPDCName() As Byte
Dim abytUserName() As Byte
Dim lngRet As Long
Dim i As Long
' Unicode
abytPDCName = fGetDCName() & vbNullChar
If (Len(strUserName) = 0) Then strUserName = fGetUserName()
abytUserName = strUserName & vbNullChar
' Level 2
lngRet = apiNetUserGetInfo( _
abytPDCName(0), _
abytUserName(0), _
2, _
pBuf)
If (lngRet = ERROR_SUCCESS) Then
Call sapiCopyMem(pTmp, ByVal pBuf, Len(pTmp))
fGetFullNameOfLoggedUser = fStrFromPtrW(pTmp.usri2_full_name)
End If
Call apiNetAPIBufferFree(pBuf)
ExitHere:
Exit Function
ErrHandler:
fGetFullNameOfLoggedUser = vbNullString
Resume ExitHere
End Function
Function fGetUserName() As String
' Returns the network login name
On Error GoTo FGUerror
Dim lngLen As Long, lngRet As Long
Dim strUserName As String
strUserName = String$(254, 0)
lngLen = 255
lngRet = apiGetUserName(strUserName, lngLen)
If lngRet Then
fGetUserName = Left$(strUserName, lngLen - 1)
End If
Exit Function
FGUerror:
MsgBox "Error getting user name: " & Err.Description
fGetUserName = ""
End Function
Function fGetDCName() As String
Dim pTmp As Long
Dim lngRet As Long
Dim abytBuf() As Byte
On Error GoTo FGDCerror
lngRet = apiNetGetDCName(0, 0, pTmp)
If lngRet = NERR_SUCCESS Then
fGetDCName = fStrFromPtrW(pTmp)
End If
Call apiNetAPIBufferFree(pTmp)
Exit Function
FGDCerror:
MsgBox "Error in fGetDCName: " & Err.Description
fGetDCName = ""
End Function
Private Function fStrFromPtrW(pBuf As Long) As String
Dim lngLen As Long
Dim abytBuf() As Byte
On Error GoTo FSFPerror
' Get the length of the string at the memory location
lngLen = apilstrlenW(pBuf) * 2
' if it's not a ZLS
If lngLen Then
ReDim abytBuf(lngLen)
' then copy the memory contents
' into a temp buffer
Call sapiCopyMem( _
abytBuf(0), _
ByVal pBuf, _
lngLen)
' return the buffer
fStrFromPtrW = abytBuf
End If
Exit Function
FSFPerror:
MsgBox "Error in fStrFromPtrW: " & Err.Description
fStrFromPtrW = ""
End Function
' ******** Code End *********
#End If
steps 3 & 4 form an HTTP request, and send it to a server; handle failure gracefully (note - right now "gracefully" involves an error message; you can comment it out, and then the user will notice just a slight delay when opening the workbook and nothing else). Paste the following code in another module (call it 'registration'):
Option Explicit
Option Compare Text
' use the name of the workbook you want to identify:
Public Const WB_NAME = "logMe 1.0"
' use the URL of the script that handles the request
' this one works for now and you can use it to test until you get your own solution up
Public Const DB_SERVER = "http://www.floris.us/SO/logUsePDO.php"
Sub registerUse()
' send http request to a server
' to log "this user is using this workbook at this time"
Dim USER_NAME As String
Dim regString As String
Dim response As String
' find the login name of the user:
USER_NAME = currentUserName()
' create a "safe" registration string by URLencoding the user name and workbook name:
regString = "?user=" & URLEncode(USER_NAME) & "&application=" & URLEncode(WB_NAME, True)
' log the use:
response = logUse(DB_SERVER & regString)
' remove the success / fail message box when you are satisfied this works; it gets annoying quickly:
If response = "user " & USER_NAME & " logged successfully" Then
MsgBox "logging successful"
Else
MsgBox "Response: " & response
End If
End Sub
'----------------------
' helper functions
' URLencode
' found at http://stackoverflow.com/a/218199/1967396
Public Function URLEncode( _
StringVal As String, _
Optional SpaceAsPlus As Boolean = False _
) As String
Dim StringLen As Long: StringLen = Len(StringVal)
If StringLen > 0 Then
ReDim result(StringLen) As String
Dim i As Long, CharCode As Integer
Dim Char As String, Space As String
If SpaceAsPlus Then Space = "+" Else Space = "%20"
For i = 1 To StringLen
Char = Mid$(StringVal, i, 1)
CharCode = Asc(Char)
Select Case CharCode
Case 97 To 122, 65 To 90, 48 To 57, 45, 46, 95, 126
result(i) = Char
Case 32
result(i) = Space
Case 0 To 15
result(i) = "%0" & Hex(CharCode)
Case Else
result(i) = "%" & Hex(CharCode)
End Select
Next i
URLEncode = Join(result, "")
End If
End Function
Function logUse(s As String)
Dim MyRequest As Object
Set MyRequest = CreateObject("WinHttp.WinHttpRequest.5.1")
On Error GoTo noLog
' MsgBox "Sending request " & s
MyRequest.Open "GET", s
' Send Request.
MyRequest.Send
'And we get this response
logUse = MyRequest.ResponseText
Exit Function
noLog:
logUse = "Error: " & Err.Description
End Function
step 5: log the request. For this I wrote a small php script that updates a table softwareReg with three columns: user, application, and date (a system generated timestamp). The use is logged by making a request of the form:
http://www.floris.us/SO/logUse.php?name=myName&application=thisApplication
where myName is the name of the user according to currentUserName() and thisApplication is the name (maybe including the version number) of the application / workbook you want to register. You can do this right from your browser if you want to try (although the idea is that the VBA script will do it for you...)
You can request a summary of use with the following request to the same page:
http://www.floris.us/SO/logUse.php?summary=thisApplication
This will create a summary table of use, with names of users and the last date of use, sorted by "most number of registrations" - in other words, the most frequent users will be at the top. Obviously you could change the format, sort order, etc - but this should fulfill your basic requirement. I obfuscated the user names, passwords etc, but otherwise this is the code that runs at the above URL. Play with it and see if you can get it to work. The same database can record registrations for multiple applications / workbooks; right now the script will spit out results for one application at a time when the argument is the name of the application, or a table of all the applications and their use when the argument is all:
http://www.floris.us/SO/logUse.php?summary=all
Will produce a table like this (for testing I used application names something and nothing):
<?php
if (isset($_GET)) {
if (isset($_GET['user']) && isset($_GET['application'])) {
$user = $_GET['user'];
$application = $_GET['application'];
$mode = 1;
}
if (isset($_GET['summary'])) {
$application = $_GET['summary'];
$mode = 2;
}
// create database handle:
$dbhost = 'localhost';
$dbname = 'LoneStar';
$dbuser = 'DarkHelmet';
$dbpass = '12345';
try {
$DBH = new PDO("mysql:host=$dbhost;dbname=$dbname", $dbuser, $dbpass);
$DBH->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING );
$STHinsert = $DBH->prepare("INSERT INTO softwareReg( user, application ) value (?, ?)");
if($mode == 1) {
$dataInsert = array($user, $application);
$STHinsert->execute($dataInsert);
echo "user " . $user . " logged successfully";
}
if($mode == 2) {
if ($application == "all") {
$astring = "";
$table_hstring = "</td><td width = 200 align = center>application";
}
else {
$astring = "WHERE application = ?";
$table_hstring = "";
}
$STHread = $DBH->prepare("SELECT user, date, max(date) as mDate, count(user) as uCount, application FROM softwareReg ".$astring." GROUP BY user, application ORDER BY application, uCount DESC");
$dataRead = array($application);
$STHread->setFetchMode(PDO::FETCH_ASSOC);
$STHread->execute($dataRead);
echo "<html><center><h1>The following is the last time these users accessed '" . $application . "'</h1><br>";
echo "<table border=1>";
echo "<t><td width = 100 align = center>user</td><td width = 200 align=center>last access</td><td width = 100 align = center>count".$table_hstring."</td></tr>";
while ($row = $STHread->fetch()){
if($application == "all") {
echo "<tr><td align = center>" . $row['user'] .
"</td><td align = center>" . $row['mDate'] .
"</td><td align = center>" . $row['uCount'] .
"</td><td align = center>" . $row['application'] . "</tr>";
}
else {
echo "<tr><td align = center>" . $row['user'] . "</td><td align = center>" . $row['mDate'] . "</td><td align = center>" . $row['uCount'] . "</tr>";
}
}
echo "</table></html>";
}
}
catch(PDOException $e) {
echo "error connecting!<br>";
echo $e->getMessage();
}
}
?>

Check this answer How to hide code in VBA applications
Apperantly you can lock VBA code. And in your VBA code you can connect to DB and run the checks for each user. Make user enter some password and make VBA close the file if user access expired.
Another question, user may turn off macros. So you need to create functionality, wich doesn't work without macros

Related

VBA Excel odd behaviour

I've been attempting to write a module that can be imported into any spreadsheet and adapted to manage user account information stored in any sheet within the workbook.
So far I've written the following module:
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' modUserSession
'
' The purpose of this module is to provide a layer that manages
' user accounts.
'
' There is a standard format that must be adheired to, this is
' done by populating the global SessionParams type with the
' relevant information describing the particular sheet where
' the user account information is stored.
'
Type SessionParameters
sheetName As String ' Name of the sheet that contains user accounts
sheetUnlock As String ' String to lock/unlock the users sheet
startRow As Integer ' Row on which user accounts begin
nameColumn As Integer ' Column on which user names are stored
passColumn As Integer ' Column on which user passwords are stored
forenameColumn As Integer ' Column where user forename is stored
surnameColumn As Integer ' Column where user surname is stored
telephoneColumn As Integer ' Column where user telephone number is stored
emailColumn As String ' Column where user email address is stored
End Type
Type UserAccount
userName As String
userPass As String
foreName As String
surName As String
telNum As String
emailAddr As String
End Type
Private SessionParams As SessionParameters
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' setSessionParams()
'
' Populates the global SessionParams that is used to reference the
' relevant sheet where user accounts are stored.
'
Public Function setSessionParams( _
ByRef sParams As SessionParameters _
)
SessionParams.sheetName = sParams.sheetName
SessionParams.sheetUnlock = sParams.sheetUnlock
SessionParams.startRow = sParams.startRow
SessionParams.nameColumn = sParams.nameColumn
SessionParams.passColumn = sParams.passColumn
SessionParams.forenameColumn = sParams.forenameColumn
SessionParams.surnameColumn = sParams.surnameColumn
SessionParams.telephoneColumn = sParams.telephoneColumn
SessionParams.emailColumn = sParams.emailColumn
End Function
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' nextEmptyUserRow()
'
' Will return the next empty row on the sheet referenced by
' the global SessionParams type.
'
Public Function nextEmptyUserRow()
Dim intRow As Integer
Dim sheetName As String
Dim nameColumn As Integer
sheetName = SessionParams.sheetName
nameColumn = SessionParams.nameColumn
intRow = SessionParams.startRow
While (True)
If (ActiveWorkbook.Sheets(sheetName).Cells(intRow, nameColumn) = "") Then
nextEmptyUserRow = intRow
Exit Function
End If
intRow = (intRow + 1)
Wend
End Function
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' findUserAccount()
'
' Finds the row on which a particular user account is stored and
' returns the row number.
'
' If the specified user account isn't found then 0 is returned.
'
Public Function findUserAccount( _
ByVal userName As String _
) As Integer
Dim intRow As Integer
Dim sheetName As String
Dim nameColumn As Integer
sheetName = SessionParams.sheetName
nameColumn = SessionParams.nameColumn
intRow = SessionParams.startRow
While (True)
If (ActiveWorkbook.Sheets(sheetName).Cells(intRow, nameColumn) = "") Then
findUserAccount = 0
Exit Function
End If
If (ActiveWorkbook.Sheets(sheetName).Cells(intRow, nameColumn) = userName) Then
findUserAccount = intRow
Exit Function
End If
intRow = (intRow + 1)
Wend
End Function
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' loadUserAccountByRow()
'
' Loads a user account from the specified row - even if the row is
' empty and contains no data, the cells are loaded and returned
' in a SessionParameters type and will be empty.
'
Public Function loadUserAccountByRow( _
ByVal intRow As Integer _
) As UserAccount
Dim sheetName As String
sheetName = SessionParams.sheetName
loadUserAccountByRow.userName = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.nameColumn)
loadUserAccountByRow.userPass = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.passColumn)
loadUserAccountByRow.foreName = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.forenameColumn)
loadUserAccountByRow.surName = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.surnameColumn)
loadUserAccountByRow.telNum = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.telephoneColumn)
loadUserAccountByRow.emailAddr = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.emailColumn)
End Function
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' loadUserAccountByName()
'
' Loads the specified user account and returns a SessionParameters
' type with the loaded info.
'
' If the specified account doesn't exist then all of the fiels in
' the SessionParameters type will be empty.
'
' This function works by calling findUserAccount(), passing the
' specified userName as a parameter.
'
' If the account doesn't exist then findUserAccount() will return
' 0, in which case an empty row is loaded.
'
' If the account does exist it is loaded via a call to the
' loadUserAccountByRow() function.
'
Public Function loadUserAccountByName( _
ByVal userName As String _
) As UserAccount
Dim intRow As Integer
' Get the row on which the user account is stored - the
' findUserAccount function will return 0 if the specified
' user account does not exist.
'
intRow = findUserAccount(userName)
If (intRow = 0) Then
' Return an empty SessionParameters structure.
'
loadUserAccountByName = loadUserAccountByRow(nextEmptyUserRow)
Else
loadUserAccountByName = loadUserAccountByRow(intRow)
End If
End Function
The idea now is that you populate the global SessionParameters variable (SessionParams defined in the module) with information relevant to the particular sheet where the data is stored. I basically need to fill the SessionParams to indicate the sheet name and the start row/columns where the data is stored.
I have the following sheet:
So, what I'm doing now is testing the loadUserAccountByName() function (from the above module).
So first I have to initialise the global SessionParams with the relevant info that points to the sheet and defines where on the sheet everything is.
So when my workbook is first opened I have the following code being executed:
Private Function populateSessionParams()
Dim sParams As SessionParameters
Dim userRow As Integer
sParams.sheetName = "Users"
sParams.sheetUnlock = "UnlockSheet"
sParams.startRow = 5
sParams.nameColumn = 4
sParams.passColumn = 6
sParams.forenameColumn = 8
sParams.surnameColumn = 10
sParams.telephoneColumn = 12
sParams.emailColumn = 14
setSessionParams sParams
End Function
Private Sub Workbook_Open()
Dim uParams As UserAccount
populateSessionParams
uParams = loadUserAccountByName("Admin")
MsgBox _
"Username: " & uParams.userName & vbCrLf & _
"Password: " & uParams.userPass & vbCrLf & _
"Forename: " & uParams.foreName & vbCrLf & _
"Surname: " & uParams.surName & vbCrLf & _
"Tel: " & uParams.telNum & vbCrLf & _
"Email: " & uParams.emailAddr
End Sub
In the Workbook_Open() function I first call the populateSessionParams() function - this initialises the global SessionParams defined in the module with appropriate values.
Everything works great except for one thing - the loadUserAccountByName() function calls on the loadUserAccountByRow() function to populate the UserAccount type with the relevant info if it finds a particular user account. And it is finding the Admin account and loading all of the relevant data except for the emailAddr member which it refuses to populate.
The error can be traced to the following line from the loadUserAccountByRow() function:
loadUserAccountByRow.emailAddr = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.emailColumn)
I can see from the debugger that SessionParams.emailColumn definitely contains the value 14 which is the column where email addresses are stored in the Users sheet. I can also see in the debugger that the value of sheetName is indeed "Users"
It populates all other values of the UserAccount type using pretty much the same code:
' Each of the following lines work.
loadUserAccountByRow.userName = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.nameColumn)
loadUserAccountByRow.userPass = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.passColumn)
loadUserAccountByRow.foreName = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.forenameColumn)
loadUserAccountByRow.surName = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.surnameColumn)
loadUserAccountByRow.telNum = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.telephoneColumn)
' This particular line will not work and I get an application or
' object defined error. If I comment this line out, everything works
' great and there are no errors.
'
loadUserAccountByRow.emailAddr = ActiveWorkbook.Sheets(sheetName).Cells(intRow, SessionParams.emailColumn)
I've been beating my brow over this for a bit now and can't seem to get my head around it. I know it's a bit long winded but you really need all of the code and a reasonable explanation of what I'm trying to do, hope it makes sense.
Any help appreciated.
SessionParameters.emailColumn is the only one defined as String: all other column positions are Integer.
That is mostly likely the source of the error.

Accessing SurveyMonkey API from VBA

I am tying to set-up a Excel VBA project to readout individual survey responses into a form in Excel for some calculations and then PDF reporting.
However I have great difficulty to deploy the .NET library (SurveyMonkeyApi) to be available for reference in VBA.
I have set up a VisualStudio project to test that way , and I can install it for that specific VS project (through NuGet PM). But the library is not made available for Excel on that machine.
I have downloaded (on another machine) the libraries through standalone NuGet and they download OK but then I am at loss on how to register for Excel VBA access. On top of it there is a dependency on NewtonsoftJson library too (which downloaded automatically on both occasions).
Good advice appreciated!
I just saw this now - is there a feature for StackOverflow to alert me when a comment is added or a question answered, so I know to look back?
Here is starting code:
Option Explicit
Public Const gACCESS_TOKEN As String = "xxxxxxxxxxxxxxxxxxxxxx"
Declare Function GetTickCount Lib "kernel32" () As Long
Declare Sub Sleep Lib "kernel32" (ByVal lngMilliSeconds As Long)
' for a JSON parser see https://code.google.com/p/vba-json/
Public Sub test()
Dim vRequestBody As Variant, sResponse As String, sSurveyID As String
sSurveyID = "1234567890"
vRequestBody = "{""survey_id"":" & """" & sSurveyID & """" _
& ", ""fields"":[""collector_id"", ""url"", ""open"", ""type"", ""name"", ""date_created"", ""date_modified""]" _
& "}"
sResponse = SMAPIRequest("get_collector_list", vRequestBody)
End Sub
Function SMAPIRequest(sRequest As String, vRequestBody As Variant) As String
Const SM_API_URI As String = "https://api.surveymonkey.net/v2/surveys/"
Const SM_API_KEY As String = "yyyyyyyyyyyyyyyyyyyyyyyy"
Dim bDone As Boolean, sMsg As String, sUrl As String, oHttp As Object ' object MSXML2.XMLHTTP
Static lsTickCount As Long
If Len(gACCESS_TOKEN) = 0 Then
Err.Raise 9999, "No Access token"
End If
On Error GoTo OnError
sUrl = SM_API_URI & URLEncode(sRequest) & "?api_key=" & SM_API_KEY
'Debug.Print Now() & " " & sUrl
Application.StatusBar = Now() & " " & sRequest & " " & Left$(vRequestBody, 127)
Set oHttp = CreateObject("MSXML2.XMLHTTP") ' or "MSXML2.ServerXMLHTTP"
Do While Not bDone ' 4.33 offer retry
If GetTickCount() - lsTickCount < 1000 Then ' if less than 1 sec since last call, throttle to avoid sResponse = "<h1>Developer Over Qps</h1>"
Sleep 1000 ' wait 1 second so we don't exceed limit of 2 qps (queries per second)
End If
lsTickCount = GetTickCount()
'Status Retrieves the HTTP status code of the request.
'statusText Retrieves the friendly HTTP status of the request.
'Note The timeout property has a default value of 0.
'If the time-out period expires, the responseText property will be null.
'You should set a time-out value that is slightly longer than the expected response time of the request.
'The timeout property may be set only in the time interval between a call to the open method and the first call to the send method.
RetryPost: ' need to do all these to retry, can't just retry .Send apparently
oHttp.Open "POST", sUrl, False ' False=not async
oHttp.setRequestHeader "Authorization", "bearer " & gACCESS_TOKEN
oHttp.setRequestHeader "Content-Type", "application/json"
oHttp.send CVar(vRequestBody) ' request body needs brackets EVEN around Variant type
'-2146697211 The system cannot locate the resource specified. => no Internet connection
'-2147024809 The parameter is incorrect.
'String would return {"status": 3, "errmsg": "No oJson object could be decoded: line 1 column 0 (char 0)"} ??
'A Workaround would be to use parentheses oHttp.send (str)
'"GET" err -2147024891 Access is denied.
'"POST" Unspecified error = needs URLEncode body? it works with it but
SMAPIRequest = oHttp.ResponseText
'Debug.Print Now() & " " & Len(SMAPIRequest) & " bytes returned"
sMsg = Len(SMAPIRequest) & " bytes returned in " & (GetTickCount() - lsTickCount) / 1000 & " seconds: " & sRequest & " " & Left$(vRequestBody, 127)
If Len(SMAPIRequest) = 0 Then
bDone = MsgBox("No data returned - do you wish to retry?" _
& vbLf & sMsg, vbYesNo, "Retry?") = vbNo
Else
bDone = True ' got reply.
End If
Loop ' Until bdone
Set oHttp = Nothing
GoTo ExitProc
OnError: ' Pass True to ask the user what to do, False to raise to caller
Select Case MsgBox(Err.Description, vbYesNoCancel, "SMAPIRequest")
Case vbYes
Resume RetryPost
Case vbRetry
Resume RetryPost
Case vbNo, vbIgnore
Resume Next
Case vbAbort
End
Case Else
Resume ExitProc ' vbCancel
End Select
ExitProc:
End Function
Public Function URLEncode(StringVal As String, Optional SpaceAsPlus As Boolean = False) As String
Dim StringLen As Long
StringLen = Len(StringVal)
If StringLen > 0 Then
ReDim result(StringLen) As String
Dim i As Long, CharCode As Integer
Dim Char As String, Space As String
If SpaceAsPlus Then Space = "+" Else Space = "%20"
For i = 1 To StringLen
Char = Mid$(StringVal, i, 1)
CharCode = Asc(Char)
Select Case CharCode
Case 97 To 122, 65 To 90, 48 To 57, 45, 46, 95, 126
result(i) = Char
Case 32
result(i) = Space
Case 0 To 15
result(i) = "%0" & Hex(CharCode)
Case Else
result(i) = "%" & Hex(CharCode)
End Select
Next i
URLEncode = Join(result, "")
End If
End Function
EDIT 23-APRIL add more code.
the Me. comes from code in a Userform.
Set jLib = New JSONLib
vRequestBody = "{"
If Me.txtDaysCreated > "" Then
vRequestBody = vRequestBody & JKeyValue("start_date", Format$(Now() - CDbl(Me.txtDaysCreated), "yyyy-mm-dd")) & ","
End If
If Me.txtTitleContains > "" Then
' title contains "text", case insensitive
vRequestBody = vRequestBody & JKeyValue("title", Me.txtTitleContains) & ","
End If
vRequestBody = vRequestBody _
& JKeyValue("fields", Array("title", "date_created", "date_modified", "num_responses", _
"language_id", "question_count", "preview_url", "analysis_url")) & "}"
'returns in this order: 0=date_modified 1=title 2=num_responses 3=date_created 4=survey_id
' and in date_created descending
sResponse = GetSMAPIResponse("get_survey_list", vRequestBody)
------------------------------------------
Function JKeyValue(sKey As String, vValues As Variant) As String
Dim jLib As New JSONLib
JKeyValue = jLib.toString(sKey) & ":" & jLib.toString(vValues)
Set jLib = Nothing
End Function
Edit 25-April overview of VBA code to get the data
This is covered in the SM documentation, but I'll sketch how that looks in VBA.
the response to get_survey_details gives you all the survey setup data. Use
Set oJson = jLib.parse(Replace(sResponse, "\r\n", " "))
to get a json object.
Set dictSurvey = oJson("data")
gives you the dictionary so you can get data like dictSurvey("num_responses"). I take it you know how to index into dictionary objects to get field values.
Set collPages = dictSurvey("pages")
gives you a collection of Pages. The undocumented field "position" gives you the order of pages in the survey UI.
For lPage = 1 To collPages.Count
Set dictPage = collPages(lPage)
Set collPageQuestions = dictPage("questions") ' gets you the Qs on this page
For lPageQuestion = 1 To collPageQuestions.Count
Set dictQuestion = collPageQuestions(lPageQuestion) ' gets you one Q
Set collAnswers = dictQuestion("answers") ' gets the QuestionOptions for this Q
For lAnswer = 1 To collAnswers.Count
Set dictAnswer = collAnswers(lAnswer) ' gets you one Question Option
etc etc
Then given the number of responses from above, loop through the respondents 100 at a time - again see the SM doc for details of how to specify start and end dates to do incremental downloads over time.
create a json object from the response to "get_respondent_list"
Collect the fields for each respondent and accumulate a list of at most 100 respondent IDs.
Then "get_responses" for that list.
Set collResponsesData = oJson("data")
For lResponse = 1 To collResponsesData.Count
If not IsNull(collResponsesData(lResponse)) then
... get fields...
Set collQuestionsAnswered = collResponsesData(lResponse)("questions")
For lQuestion = 1 To collQuestionsAnswered.Count
Set dictQuestion = collQuestionsAnswered(lQuestion)
nQuestion_ID = CDbl(dictQuestion("question_id"))
Set collAnswers = dictQuestion("answers") ' this is a collection of dictionaries
For lAnswer = 1 To collAnswers.Count
On Error Resume Next ' only some of these may be present
nRow = 0: nRow = CDbl(collAnswers(lAnswer)("row"))
nCol = 0: nCol = CDbl(collAnswers(lAnswer)("col"))
nCol_choice = 0: nCol_choice = CDbl(collAnswers(lAnswer)("col_choice"))
sText = "": sText = collAnswers(lAnswer)("text")
nValue = 0: nValue = Val(sText)
On Error GoTo 0
and save all those values in a recordset or sheet or whatever
Hope that helps.
I access the SM API in straight VBA.
Just CreateObject("MSXML2.XMLHTTP") then issue calls and use the SimpleJsON JSONLib to parse it.
If I wanted to access VB.Net code, I'd package it with ExcelDNA to create a XLL and that gives a straight Excel addin.
I would think you would need to add it into the References for your Excel project.
From the Ribbon, select, Tools, then References, then scroll through the list looking for something about SurveyMonkey API.
So encouraged by #sysmod I have tried to do something in VBA directly. I have left out the JSON for now as I am already in trouble. The below is giving me "Developer Inactive" as a result, though I have another project in VB.NET where the same key and token works fine.
Public Sub GetSMList()
Dim apiKey As String
Dim Token As String
Dim sm As Object
apiKey = "myKey"
Token = "myToken"
Set sm = CreateObject("MSXML2.XMLHTTP.6.0")
With sm
.Open "POST", "https://api.surveymonkey.net/v2/surveys/get_survey_list", False
.setRequestHeader "Authorization", "Bearer " & Token
.setRequestHeader "Content-Type", "application/json"
.send "api_key=" & apiKey
result = .responseText
End With
End Sub

resolve loading USE or USELSX error from script

i'm working with lotus notes 6.5.6 and sometimes, when users try to exegute an agent notes retrive "loading USE or USELSX module" error so, i have to go into the library that is used my that agent and re-save it. After that the agent work. I don't know the cause of this error but i'm studying for understand it, meantime i have to find a solution that allow users to solve the problem without my presence.
So i'm look for a solution that allow users to access the library and resave it to solve that error.
I try to do this:
Dim db As NotesDatabase
Dim session As New NotesSession
Dim doclib As NotesDocument
Dim colllib As NotesNoteCollection
Dim id As String
Set db=session.CurrentDatabase
Set colllib = db.CreateNoteCollection(False)
colllib.SelectScriptLibraries=True
Call colllib.BuildCollection()
Id=colllib.GetFirstNoteId
While Not Id=""
Set doclib=db.GetDocumentByID(Id)
If Not doclib Is Nothing Then
Call doclib.ComputeWithForm(False,False)
Call doclib.Save(True,False)
End If
id=colllib.GetNextNoteId(Id)
Wend
but the error is not resolved.
How can i do ?
thank's
In order to resolve your issue you would need to recompile the script (what is automatically done when saving it in the Designer- Client) and then programmatically sign it (what is only possible using the C-API). Calling a "ComputeWithForm" on a Design- Element does NOT recompile the script.
Rather than fixing something that should not be broken, I would rather investigate why this agent stops running.
Usually this happens, when the designer- task "merges" script- libraries / agents from different templates to one database.
I am quite sure, that the reason for your agent not working anymore is the nightly designer task. Try to take out the template- name from the "broken" database or manipulate it (add an "x" or something) so that you keep the information, but designer task does not find the corresponding template. This should fix your problem.
If you really cannot fix the problem with this approach, then try this code I found here, put it in an agent and let the user call it via another agent with ag.RunOnServer (then it runs with more rights than the user has):
'LS Recompile:
Option Public
Option Explicit
%REM
An example of using Notes API calls to recompile LotusScript.
version 1.0
Julian Robichaux
http://www.nsftools.com
%END REM
'** Notes C-API functions
Declare Function OSPathNetConstruct Lib "nnotes.dll" (Byval portName As Integer, _
Byval serverName As String, Byval fileName As String, Byval pathName As String) As Integer
Declare Function NSFDbOpen Lib "nnotes.dll" (Byval dbName As String, hDb As Long) As Integer
Declare Function NSFDbClose Lib "nnotes.dll" (Byval hDb As Long) As Integer
Declare Function NSFNoteLSCompile Lib "nnotes.dll" (Byval hDb As Long, _
Byval hNote As Long, Byval dwFlags As Long) As Integer
Declare Function NSFNoteSign Lib "nnotes.dll" (Byval hNote As Long) As Integer
Declare Function NSFNoteUpdate Lib "nnotes.dll" (Byval hNote As Long, _
Byval flags As Integer) As Integer
Declare Function OSLoadString Lib "nnotes.dll" (Byval hModule As Long, Byval stringCode As Integer, _
Byval retBuffer As String, Byval bufferLength As Integer) As Integer
'================================================================
' Base class for working with Notes databases at the API level
'================================================================
Class APIBaseClass
Private db As NotesDatabase
Private hDb As Long
Private lastError As String
Public Sub New ()
'** nothing to instantiate in the base class
End Sub
Public Sub Delete ()
Call CloseDatabase()
End Sub
Public Function OpenDatabase (db As NotesDatabase) As Integer
On Error Goto processError
If (hDb > 0) Then
Call CloseDatabase()
End If
'** reset the internals
Set Me.db = db
lastError = ""
Dim pathName As String*256
Dim result As Integer
'** create a proper network path name with OSPathNetConstruct
Call OSPathNetConstruct(0, db.Server, db.FilePath, pathName)
'** open the database and get a handle with NSFDbOpen
result = NSFDbOpen(pathName, hDb)
If result = 0 Then
OpenDatabase = True
Else
Call SetLastError("Cannot open database " & db.FilePath & " on server " & db.Server, result)
End If
Exit Function
processError:
Call SetLastError("Error opening database", 0)
Exit Function
End Function
Public Sub CloseDatabase ()
On Error Resume Next
If (hDb > 0) Then
Call NSFDbClose(hDb)
End If
Set db = Nothing
hDb = 0
lastError = ""
End Sub
Private Function SetLastError (errText As String, apiResultCode As Integer) As String
If (apiResultCode <> 0) Then
LastError = "API Error " & apiResultCode & ": " & GetAPIError(apiResultCode)
Elseif (Err > 0) Then
LastError = "Notes Error " & Err & ": " & Error$
Else
LastError = ""
End If
If (Len(errText) > 0) Then
LastError = errText & ". " & LastError
End If
End Function
Public Function GetLastError () As String
GetLastError = LastError
End Function
Public Function GetAPIError (errorCode As Integer) As String
Dim errorString As String*256
Dim returnErrorString As String
Dim resultStringLength As Long
Dim errorCodeTranslated As Integer
Const ERR_MASK = &H3fff
Const PKG_MASK = &H3f00
Const ERRNUM_MASK = &H00ff
'** mask off the top 2 bits of the errorCode that was returned; this is
'** what the ERR macro in the API does
errorCodeTranslated = (errorCode And ERR_MASK)
'** get the error code translation using the OSLoadString API function
resultStringLength = OSLoadString(0, errorCodeTranslated, errorString, Len(errorString) - 1)
'** strip off the null-termination on the string before you return it
If (Instr(errorString, Chr(0)) > 0) Then
returnErrorString = Left$(errorString, Instr(errorString, Chr(0)) - 1)
Else
returnErrorString = errorString
End If
GetAPIError = returnErrorString
End Function
End Class
'================================================================
' Special subclass for recompiling a note/doc in a database
'================================================================
Class LotusScriptRecompiler As APIBaseClass
Public Function RecompileLSByNoteID (noteID As String) As Integer
On Error Goto processError
If (db Is Nothing) Then
Call SetLastError("Database is not open", 0)
Exit Function
End If
Dim doc As NotesDocument
Set doc = db.GetDocumentByID(noteID)
RecompileLSByNoteID = RecompileLS(doc)
Exit Function
processError:
Call SetLastError("Error recompiling LotusScript for " & noteID, 0)
Exit Function
End Function
Public Function RecompileLSByUNID (unid As String) As Integer
On Error Goto processError
If (db Is Nothing) Then
Call SetLastError("Database is not open", 0)
Exit Function
End If
Dim doc As NotesDocument
Set doc = db.GetDocumentByUNID(unid)
RecompileLSByUNID = RecompileLS(doc)
Exit Function
processError:
Call SetLastError("Error recompiling LotusScript for " & unid, 0)
Exit Function
End Function
Public Function RecompileLS (doc As NotesDocument) As Integer
On Error Goto processError
Dim hNote As Long
Dim unid As String
Dim result As Integer
If (hDb = 0) Then
Call SetLastError("Database is not open", 0)
Exit Function
Elseif (doc Is Nothing) Then
Call SetLastError("Invalid document reference", 0)
Exit Function
End If
'** super-special-secret way of getting an API handle to a NotesDocument
hNote = doc.Handle
unid = doc.UniversalID
'** first, we compile the note
result = NSFNoteLSCompile(hDb, hNote, 0)
If (result <> 0) Then
Call SetLastError("Cannot compile LotusScript for " & GetTitle(doc), result)
Exit Function
End If
'** then we sign it
result = NSFNoteSign(hNote)
If (result <> 0) Then
Call SetLastError("Cannot sign " & GetTitle(doc), result)
Exit Function
End If
'** then we save it
result = NSFNoteUpdate(hNote, 0)
If (result <> 0) Then
Call SetLastError("Cannot save " & GetTitle(doc), result)
Exit Function
End If
'** update the in-memory reference to the object
Delete doc
Set doc = db.GetDocumentByUNID(unid)
'** a little trick to avoid this message on recompiled forms:
'** This document has been altered since the last time it was signed! Intentional tampering may have occurred.
Call doc.Sign()
Call doc.Save(True, False)
lastError = ""
RecompileLS = True
Exit Function
processError:
Call SetLastError("Error recompiling LotusScript for " & GetTitle(doc), 0)
Exit Function
End Function
Public Function GetTitle (doc As NotesDocument) As String
On Error Resume Next
If (doc Is Nothing) Then
Exit Function
End If
Dim title As String
title = doc.~$Title(0)
If (Instr(title, "|") > 0) Then
title = Strleft(title, "|")
End If
If (title = "") Then
title = "(untitled)"
End If
GetTitle = |"| & title & |"|
End Function
End Class
Sub Initialize
'** As a test, let's recompile all the agents, script libraries, and forms
'** in this database
Dim session As New NotesSession
Dim db As NotesDatabase
Dim nc As NotesNoteCollection
Dim recompiler As New LotusScriptRecompiler
Dim noteID As String
'** create our recompiler object
Set db = session.CurrentDatabase
Call recompiler.OpenDatabase(db)
If (recompiler.GetLastError <> "") Then
Print recompiler.GetLastError
Exit Sub
End If
'** compile the script libraries first (note that this will NOT build a
'** dependency tree -- rather, we'll try to brute-force around the
'** dependencies by recompiling until either (A) there are no errors,
'** or (B) the number of errors we get is the same as we got last time)
Dim errCount As Integer, lastCount As Integer
Set nc = db.CreateNoteCollection(False)
nc.SelectScriptLibraries = True
Call nc.BuildCollection
Print "SCRIPT LIBRARIES"
Do
lastCount = errCount
errCount = 0
noteID = nc.GetFirstNoteId
Do Until (noteID = "")
If recompiler.RecompileLSByNoteID(noteID) Then
Print "Successfully recompiled " & _
recompiler.GetTitle(db.GetDocumentByID(noteID))
Else
Print recompiler.GetLastError
errCount = errCount + 1
End If
noteID = nc.GetNextNoteId(noteID)
Loop
Loop Until ( (errCount = 0) Or (errCount = lastCount) )
'** then compile everything else
Set nc = db.CreateNoteCollection(False)
nc.SelectAgents = True
nc.SelectForms = True
Call nc.BuildCollection
Print "FORMS AND AGENTS"
noteID = nc.GetFirstNoteId
Do Until (noteID = "")
If recompiler.RecompileLSByNoteID(noteID) Then
Print "Successfully recompiled " & _
recompiler.GetTitle(db.GetDocumentByID(noteID))
Else
Print recompiler.GetLastError
End If
noteID = nc.GetNextNoteId(noteID)
Loop
Call recompiler.CloseDatabase()
Print "All done"
End Sub

Retrieve location of copied cell range in VBA

I'd like to be able to copy a cell and paste ONLY number formats. Unfortunately, there is no built-in option in the PasteSpecial command.
Is there a way to press the copy button, select some destination cells, run a macro, and be able to retrieve the copied cells in a way analogous to the Selection object in VBA so that I can use its properties?
The only alternative I can think of is pasting to a known empty range (very far away) and then using that intermediate range, as below:
Dim A As Range
Set A = Range("ZZ99999")
A.PasteSpecial Paste:=xlPasteAll
Selection.NumberFormat = A.NumberFormat
Thanks!
Find olelib.tlb on the Internet (Edanmo's OLE interfaces & functions). There should be plenty of download links. Download and reference from your VBA project (Tools - References).
Note that it does not contain any executable code, only declarations of OLE functions and interfaces.
Also you might notice it's quite big, about 550kb. You can extract only the needed interfaces from it and recompile to get a much lighter TLB file, but that is up to you.
(If you are really unhappy with a TLB, there is also the dark magic route where you don't need any TLBs at all because you create assembly stubs on the fly to call vTable methods directly, but I won't be feeling like porting the below code this way.)
Then create a helper module and put this code into it:
Option Explicit
' No point in #If VBA7 and PtrSafe, as the Edanmo's olelib is 32-bit
Private Declare Function OpenClipboard Lib "user32.dll" (ByVal hwnd As Long) As Long
Private Declare Function GetClipboardData Lib "user32.dll" (ByVal wFormat As Long) As Long
Private Declare Function CloseClipboard Lib "user32.dll" () As Long
Public Function GetCopiedRange() As Excel.Range
Dim CF_LINKSOURCE As Long
CF_LINKSOURCE = olelib.RegisterClipboardFormat("Link Source")
If CF_LINKSOURCE = 0 Then Err.Raise 5, , "Failed to obtain clipboard format CF_LINKSOURCE"
If OpenClipboard(0) = 0 Then Err.Raise 5, , "Failed to open clipboard."
On Error GoTo cleanup
Dim hGlobal As Long
hGlobal = GetClipboardData(CF_LINKSOURCE)
If hGlobal = 0 Then Err.Raise 5, , "Failed to get data from clipboard."
Dim pStream As olelib.IStream
Set pStream = olelib.CreateStreamOnHGlobal(hGlobal, 0)
Dim IID_Moniker As olelib.UUID
olelib.CLSIDFromString "{0000000f-0000-0000-C000-000000000046}", IID_Moniker
Dim pMoniker As olelib.IMoniker
olelib.OleLoadFromStream pStream, IID_Moniker, pMoniker
Set GetCopiedRange = RangeFromCompositeMoniker(pMoniker)
cleanup:
Set pMoniker = Nothing 'To make sure moniker releases before the stream
CloseClipboard
If Err.Number > 0 Then Err.Raise Err.Number, Err.Source, Err.Description, Err.HelpFile, Err.HelpContext
End Function
Private Function RangeFromCompositeMoniker(ByVal pCompositeMoniker As olelib.IMoniker) As Excel.Range
Dim monikers() As olelib.IMoniker
monikers = SplitCompositeMoniker(pCompositeMoniker)
If UBound(monikers) - LBound(monikers) + 1 <> 2 Then Err.Raise 5, , "Invalid composite moniker."
Dim binding_context As olelib.IBindCtx
Set binding_context = olelib.CreateBindCtx(0)
Dim WorkbookUUID As olelib.UUID
olelib.CLSIDFromString "{000208DA-0000-0000-C000-000000000046}", WorkbookUUID
Dim wb As Excel.Workbook
monikers(LBound(monikers)).BindToObject binding_context, Nothing, WorkbookUUID, wb
Dim pDisplayName As Long
pDisplayName = monikers(LBound(monikers) + 1).GetDisplayName(binding_context, Nothing)
Dim raw_range_name As String ' Contains address in the form of "!SheetName!R1C1Local", need to convert to non-local
raw_range_name = olelib.SysAllocString(pDisplayName)
olelib.CoGetMalloc(1).Free pDisplayName
Dim split_range_name() As String
split_range_name = Split(raw_range_name, "!")
Dim worksheet_name As String, range_address As String
worksheet_name = split_range_name(LBound(split_range_name) + 1)
range_address = Application.ConvertFormula(ConvertR1C1LocalAddressToR1C1(split_range_name(LBound(split_range_name) + 2)), xlR1C1, xlA1)
Set RangeFromCompositeMoniker = wb.Worksheets(worksheet_name).Range(range_address)
End Function
Private Function SplitCompositeMoniker(ByVal pCompositeMoniker As olelib.IMoniker) As olelib.IMoniker()
Dim MonikerList As New Collection
Dim enumMoniker As olelib.IEnumMoniker
Set enumMoniker = pCompositeMoniker.Enum(True)
If enumMoniker Is Nothing Then Err.Raise 5, , "IMoniker is not composite"
Dim currentMoniker As olelib.IMoniker
Do While enumMoniker.Next(1, currentMoniker) = olelib.S_OK
MonikerList.Add currentMoniker
Loop
If MonikerList.Count > 0 Then
Dim res() As olelib.IMoniker
ReDim res(1 To MonikerList.Count)
Dim i As Long
For i = 1 To MonikerList.Count
Set res(i) = MonikerList(i)
Next
SplitCompositeMoniker = res
Else
Err.Raise 5, , "No monikers found in the composite moniker."
End If
End Function
Private Function ConvertR1C1LocalAddressToR1C1(ByVal R1C1LocalAddress As String) As String
' Being extra careful here and not doing simple Replace(Replace()),
' because e.g. non-localized row letter may be equal to localized column letter which will lead to double replace.
Dim row_letter_local As String, column_letter_local As String
row_letter_local = Application.International(xlUpperCaseRowLetter)
column_letter_local = Application.International(xlUpperCaseColumnLetter)
Dim row_letter_pos As Long, column_letter_pos As Long
row_letter_pos = InStr(1, R1C1LocalAddress, row_letter_local, vbTextCompare)
column_letter_pos = InStr(1, R1C1LocalAddress, column_letter_local, vbTextCompare)
If row_letter_pos = 0 Or column_letter_pos = 0 Or row_letter_pos >= column_letter_pos Then Err.Raise 5, , "Invalid R1C1Local address"
If Len(row_letter_local) = 1 And Len(column_letter_local) = 1 Then
Mid$(R1C1LocalAddress, row_letter_pos, 1) = "R"
Mid$(R1C1LocalAddress, column_letter_pos, 1) = "C"
ConvertR1C1LocalAddressToR1C1 = R1C1LocalAddress
Else
ConvertR1C1LocalAddressToR1C1 = "R" & Mid$(R1C1LocalAddress, row_letter_pos + Len(row_letter_local), column_letter_pos - (row_letter_pos + Len(row_letter_local))) & "C" & Mid$(R1C1LocalAddress, column_letter_pos + Len(column_letter_local))
End If
End Function
Credits go to Alexey Merson.
Here's one way. Obviously you'll have to change the range to suit your situation, but it should get you the general idea:
Dim foo As Variant
foo = Sheet1.Range("A1:A10").NumberFormat
Sheet1.Range("D1:D10").NumberFormat = foo
Which really can be simplified to:
Sheet1.Range("D1:D10").NumberFormat = Sheet1.Range("A1:A10").NumberFormat
and if all of your formats in the range are the same, you can just do:
Sheet1.Range("D1:D10").NumberFormat = Sheet1.Range("A1").NumberFormat
Enough rambling...you get the idea.

Access VBA: how to return path of file you browsed to

I want to return the entire path of an excel file I browsed to.
Using the following,
Set dlg = Application.FileDialog(msoFileDialogFilePicker)
dlg.Title = "Select Excel Spreadsheet to import"
dlg.AllowMultiSelect = False
If dlg.Show = -1 Then
dataPath = dlg.InitialFileName
Me!browseDataPath = dlg.InitialFileName
End If
I'm able to open the dialog and return the directory in which the file is located, but this code doesn't append the name of the file (e.g. blabla.xls) at the end of the path.
For example, if there is blabla.xls my C drive, it will simply return C:\
How do I get it to return C:\blabla.xls (or whatever the name of the excel file is)?
Thanks!
dataPath = dlg.SelectedItems(1)
Me!browseDataPath = dataPath
As you have multi-select disabled, getting the first (one-based) item is enough.
'Paste this code in the module
Option Compare Database
'***************** Code Start **************
'This code was originally written by Ken Getz.
'It is not to be altered or distributed,
'except as part of an application.
'You are free to use it in any application,
'provided the copyright notice is left unchanged.
'
' Code courtesy of:
' Microsoft Access 95 How-To
' Ken Getz and Paul Litwin
' Waite Group Press, 1996
Type tagOPENFILENAME
lStructSize As Long
hwndOwner As Long
hInstance As Long
strFilter As String
strCustomFilter As String
nMaxCustFilter As Long
nFilterIndex As Long
strFile As String
nMaxFile As Long
strFileTitle As String
nMaxFileTitle As Long
strInitialDir As String
strTitle As String
Flags As Long
nFileOffset As Integer
nFileExtension As Integer
strDefExt As String
lCustData As Long
lpfnHook As Long
lpTemplateName As String
End Type
Declare Function aht_apiGetOpenFileName Lib "comdlg32.dll" _
Alias "GetOpenFileNameA" (OFN As tagOPENFILENAME) As Boolean
Declare Function aht_apiGetSaveFileName Lib "comdlg32.dll" _
Alias "GetSaveFileNameA" (OFN As tagOPENFILENAME) As Boolean
Declare Function CommDlgExtendedError Lib "comdlg32.dll" () As Long
Global Const ahtOFN_READONLY = &H1
Global Const ahtOFN_OVERWRITEPROMPT = &H2
Global Const ahtOFN_HIDEREADONLY = &H4
Global Const ahtOFN_NOCHANGEDIR = &H8
Global Const ahtOFN_SHOWHELP = &H10
' You won't use these.
'Global Const ahtOFN_ENABLEHOOK = &H20
'Global Const ahtOFN_ENABLETEMPLATE = &H40
'Global Const ahtOFN_ENABLETEMPLATEHANDLE = &H80
Global Const ahtOFN_NOVALIDATE = &H100
Global Const ahtOFN_ALLOWMULTISELECT = &H200
Global Const ahtOFN_EXTENSIONDIFFERENT = &H400
Global Const ahtOFN_PATHMUSTEXIST = &H800
Global Const ahtOFN_FILEMUSTEXIST = &H1000
Global Const ahtOFN_CREATEPROMPT = &H2000
Global Const ahtOFN_SHAREAWARE = &H4000
Global Const ahtOFN_NOREADONLYRETURN = &H8000
Global Const ahtOFN_NOTESTFILECREATE = &H10000
Global Const ahtOFN_NONETWORKBUTTON = &H20000
Global Const ahtOFN_NOLONGNAMES = &H40000
' New for Windows 95
Global Const ahtOFN_EXPLORER = &H80000
Global Const ahtOFN_NODEREFERENCELINKS = &H100000
Global Const ahtOFN_LONGNAMES = &H200000
Function TestIt()
Dim strFilter As String
Dim lngFlags As Long
strFilter = ahtAddFilterItem(strFilter, "Access Files (*.mda, *.mdb)", _
"*.MDA;*.MDB")
strFilter = ahtAddFilterItem(strFilter, "dBASE Files (*.dbf)", "*.DBF")
strFilter = ahtAddFilterItem(strFilter, "Text Files (*.txt)", "*.TXT")
strFilter = ahtAddFilterItem(strFilter, "All Files (*.*)", "*.*")
MsgBox "You selected: " & ahtCommonFileOpenSave(InitialDir:="C:\", _
Filter:=strFilter, FilterIndex:=3, Flags:=lngFlags, _
DialogTitle:="Hello! Open Me!")
' Since you passed in a variable for lngFlags,
' the function places the output flags value in the variable.
Debug.Print Hex(lngFlags)
End Function
Function GetOpenFile(Optional varDirectory As Variant, _
Optional varTitleForDialog As Variant) As Variant
' Here's an example that gets an Access database name.
Dim strFilter As String
Dim lngFlags As Long
Dim varFileName As Variant
' Specify that the chosen file must already exist,
' don't change directories when you're done
' Also, don't bother displaying
' the read-only box. It'll only confuse people.
lngFlags = ahtOFN_FILEMUSTEXIST Or _
ahtOFN_HIDEREADONLY Or ahtOFN_NOCHANGEDIR
If IsMissing(varDirectory) Then
varDirectory = ""
End If
If IsMissing(varTitleForDialog) Then
varTitleForDialog = ""
End If
' Define the filter string and allocate space in the "c"
' string Duplicate this line with changes as necessary for
' more file templates.
strFilter = ahtAddFilterItem(strFilter, _
"Access (*.mdb)", "*.MDB;*.MDA")
' Now actually call to get the file name.
varFileName = ahtCommonFileOpenSave( _
OpenFile:=True, _
InitialDir:=varDirectory, _
Filter:=strFilter, _
Flags:=lngFlags, _
DialogTitle:=varTitleForDialog)
If Not IsNull(varFileName) Then
varFileName = TrimNull(varFileName)
End If
GetOpenFile = varFileName
End Function
Function ahtCommonFileOpenSave( _
Optional ByRef Flags As Variant, _
Optional ByVal InitialDir As Variant, _
Optional ByVal Filter As Variant, _
Optional ByVal FilterIndex As Variant, _
Optional ByVal DefaultExt As Variant, _
Optional ByVal FileName As Variant, _
Optional ByVal DialogTitle As Variant, _
Optional ByVal hWnd As Variant, _
Optional ByVal OpenFile As Variant) As Variant
' This is the entry point you'll use to call the common
' file open/save dialog. The parameters are listed
' below, and all are optional.
'
' In:
' Flags: one or more of the ahtOFN_* constants, OR'd together.
' InitialDir: the directory in which to first look
' Filter: a set of file filters, set up by calling
' AddFilterItem. See examples.
' FilterIndex: 1-based integer indicating which filter
' set to use, by default (1 if unspecified)
' DefaultExt: Extension to use if the user doesn't enter one.
' Only useful on file saves.
' FileName: Default value for the file name text box.
' DialogTitle: Title for the dialog.
' hWnd: parent window handle
' OpenFile: Boolean(True=Open File/False=Save As)
' Out:
' Return Value: Either Null or the selected filename
Dim OFN As tagOPENFILENAME
Dim strFileName As String
Dim strFileTitle As String
Dim fResult As Boolean
' Give the dialog a caption title.
If IsMissing(InitialDir) Then InitialDir = CurDir
If IsMissing(Filter) Then Filter = ""
If IsMissing(FilterIndex) Then FilterIndex = 1
If IsMissing(Flags) Then Flags = 0&
If IsMissing(DefaultExt) Then DefaultExt = ""
If IsMissing(FileName) Then FileName = ""
If IsMissing(DialogTitle) Then DialogTitle = ""
If IsMissing(hWnd) Then hWnd = Application.hWndAccessApp
If IsMissing(OpenFile) Then OpenFile = True
' Allocate string space for the returned strings.
strFileName = Left(FileName & String(256, 0), 256)
strFileTitle = String(256, 0)
' Set up the data structure before you call the function
With OFN
.lStructSize = Len(OFN)
.hwndOwner = hWnd
.strFilter = Filter
.nFilterIndex = FilterIndex
.strFile = strFileName
.nMaxFile = Len(strFileName)
.strFileTitle = strFileTitle
.nMaxFileTitle = Len(strFileTitle)
.strTitle = DialogTitle
.Flags = Flags
.strDefExt = DefaultExt
.strInitialDir = InitialDir
' Didn't think most people would want to deal with
' these options.
.hInstance = 0
'.strCustomFilter = ""
'.nMaxCustFilter = 0
.lpfnHook = 0
'New for NT 4.0
.strCustomFilter = String(255, 0)
.nMaxCustFilter = 255
End With
' This will pass the desired data structure to the
' Windows API, which will in turn it uses to display
' the Open/Save As Dialog.
If OpenFile Then
fResult = aht_apiGetOpenFileName(OFN)
Else
fResult = aht_apiGetSaveFileName(OFN)
End If
' The function call filled in the strFileTitle member
' of the structure. You'll have to write special code
' to retrieve that if you're interested.
If fResult Then
' You might care to check the Flags member of the
' structure to get information about the chosen file.
' In this example, if you bothered to pass in a
' value for Flags, we'll fill it in with the outgoing
' Flags value.
If Not IsMissing(Flags) Then Flags = OFN.Flags
ahtCommonFileOpenSave = TrimNull(OFN.strFile)
Else
ahtCommonFileOpenSave = vbNullString
End If
End Function
Function ahtAddFilterItem(strFilter As String, _
strDescription As String, Optional varItem As Variant) As String
' Tack a new chunk onto the file filter.
' That is, take the old value, stick onto it the description,
' (like "Databases"), a null character, the skeleton
' (like "*.mdb;*.mda") and a final null character.
If IsMissing(varItem) Then varItem = "*.*"
ahtAddFilterItem = strFilter & _
strDescription & vbNullChar & _
varItem & vbNullChar
End Function
Private Function TrimNull(ByVal strItem As String) As String
Dim intPos As Integer
intPos = InStr(strItem, vbNullChar)
If intPos > 0 Then
TrimNull = Left(strItem, intPos - 1)
Else
TrimNull = strItem
End If
End Function
'************** Code End *****************
**'Now paste this part on the button click event:**
Private Sub cmd_file_Click()
Dim s_Filter As String
Dim s_InputFileName As String
s_Filter = ahtAddFilterItem(s_Filter, "Excel Files (*.XLS)", "*.XLS")
s_InputFileName = ahtCommonFileOpenSave( _
Filter:=s_Filter, OpenFile:=True, _
DialogTitle:="Please select an input file...", _
Flags:=ahtOFN_HIDEREADONLY)
Me.txt_file.Value = s_InputFileName
End Sub
Paste this code in the module.
Function GetFolder(strPath As String) As String
Dim fldr As FileDialog
Set fldr = Application.FileDialog(msoFileDialogFolderPicker)
With fldr
.Title = "Select a Folder"
.AllowMultiSelect = False
.InitialFileName = strPath
If .Show <> -1 Then GoTo NextCode
sItem = .SelectedItems(1)
End With
NextCode:
GetFolder = sItem
Sheets("Home").OLEObjects("TextBox1").Object.Value = sItem
Set fldr = Nothing
End Function
'**And then call it by using**
Private sub button1_click()
call GetFolder("Any default folder path")
end sub

Resources