I'm trying to load a file in a VBA macro that has been copied from, say, an Explorer window.
I can easily get the data from the clipboard using DataObject::GetFromClipboard, but the VBA interface to DataObject doesn't seem to have methods for working with any other formats than plain text. There are only GetText and SetText methods.
If I can't get a file stream directly from the DataObject, the filename(s) would also do, so maybe GetText could be forced to return the name of a file placed on the clipboard?
There is very little documentation to be found for VBA anywhere. :(
Maybe someone could point me to an API wrapper class for VBA that has this sort of functionality?
This works for me (in a module);
Private Declare Function IsClipboardFormatAvailable Lib "user32" (ByVal uFormat As Long) As Long
Private Declare Function OpenClipboard Lib "user32" (ByVal Hwnd As Long) As Long
Private Declare Function GetClipboardData Lib "user32" (ByVal uFormat As Long) As Long
Private Declare Function CloseClipboard Lib "user32" () As Long
Private Declare Function DragQueryFile Lib "shell32.dll" Alias "DragQueryFileA" (ByVal drop_handle As Long, ByVal UINT As Long, ByVal lpStr As String, ByVal ch As Long) As Long
Private Const CF_HDROP As Long = 15
Public Function GetFiles(ByRef fileCount As Long) As String()
Dim hDrop As Long, i As Long
Dim aFiles() As String, sFileName As String * 1024
fileCount = 0
If Not CBool(IsClipboardFormatAvailable(CF_HDROP)) Then Exit Function
If Not CBool(OpenClipboard(0&)) Then Exit Function
hDrop = GetClipboardData(CF_HDROP)
If Not CBool(hDrop) Then GoTo done
fileCount = DragQueryFile(hDrop, -1, vbNullString, 0)
ReDim aFiles(fileCount - 1)
For i = 0 To fileCount - 1
DragQueryFile hDrop, i, sFileName, Len(sFileName)
aFiles(i) = Left$(sFileName, InStr(sFileName, vbNullChar) - 1)
Next
GetFiles = aFiles
done:
CloseClipboard
End Function
Use:
Sub wibble()
Dim a() As String, fileCount As Long, i As Long
a = GetFiles(fileCount)
If (fileCount = 0) Then
MsgBox "no files"
Else
For i = 0 To fileCount - 1
MsgBox "found " & a(i)
Next
End If
End Sub
Save the files if they are in the clipboard to the destination folder.
Public Declare PtrSafe Function IsClipboardFormatAvailable Lib "user32" (ByVal wFormat As Long) As Long
Public Const CF_HDROP As Long = 15
Public Function SaveFilesFromClipboard(DestinationFolder As String) As Boolean
SaveFilesFromClipboard = False
If Not CBool(IsClipboardFormatAvailable(CF_HDROP)) Then Exit Function
CreateObject("Shell.Application").Namespace(CVar(DestinationFolder)).self.InvokeVerb "Paste"
SaveFilesFromClipboard = True
End Function
Seems like a strange way to try to get at the textfile. The DataObject class is only for working with text strings to and from the clipboard.
Here is a very good resource of that:
http://www.cpearson.com/excel/Clipboard.aspx
If your wanting to get a file stream of a file you can look into the FileSystemObject and TextStream Classes.
Related
I don't like uninitialized VBA arrays, since it's necessary to check if array is initialized, each time prior using UBound() or For Each to avoid an exception, and there is no native VBA function to check it. That is why I initialize arrays, at least doing them empty with a = Array(). This eliminates the need for extra check in most of cases, so there are no problems with 1d arrays.
For the same reason I tried to create an empty 2d array. It's not possible simply do ReDim a(0 To -1, 0 To 0), transpose 1d empty array or something similar. The only way I came across by chance, is to use MSForms.ComboBox, assign empty array to .List property and read it back. Here is the example, which works in Excel and Word, you need to insert UserForm to VBA Project, place ComboBox on it, and add the below code:
Private Sub ComboBox1_Change()
Dim a()
ComboBox1.List = Array()
a = ComboBox1.List
Debug.Print "1st dimension upper bound = " & UBound(a, 1)
Debug.Print "2nd dimension upper bound = " & UBound(a, 2)
End Sub
After combo change the output is:
1st dimension upper bound = -1
2nd dimension upper bound = 0
Actually it's really the empty 2d array in debug:
Is there more elegant way to create an empty 2d array, without using ComboBox, or UserForm controls in general?
This is only going to work for Windows (not for Mac):
Option Explicit
#If Mac Then
#Else
#If VBA7 Then
Private Declare PtrSafe Function SafeArrayCreate Lib "OleAut32.dll" (ByVal vt As Integer, ByVal cDims As Long, ByRef rgsabound As SAFEARRAYBOUND) As LongPtr
Private Declare PtrSafe Function VariantCopy Lib "OleAut32.dll" (pvargDest As Any, pvargSrc As Any) As Long
Private Declare PtrSafe Function SafeArrayDestroy Lib "OleAut32.dll" (ByVal psa As LongPtr) As Long
#Else
Private Declare Function SafeArrayCreate Lib "OleAut32.dll" (ByVal vt As Integer, ByVal cDims As Long, ByRef rgsabound As SAFEARRAYBOUND) As Long
Private Declare Function VariantCopy Lib "OleAut32.dll" (pvargDest As Variant, pvargSrc As Any) As Long
Private Declare Function SafeArrayDestroy Lib "OleAut32.dll" (ByVal psa As Long) As Long
#End If
#End If
Private Type SAFEARRAYBOUND
cElements As Long
lLbound As Long
End Type
Private Type tagVariant
vt As Integer
wReserved1 As Integer
wReserved2 As Integer
wReserved3 As Integer
#If VBA7 Then
ptr As LongPtr
#Else
ptr As Long
#End If
End Type
Public Function EmptyArray(ByVal numberOfDimensions As Long, ByVal vType As VbVarType) As Variant
'In Visual Basic, you can declare arrays with up to 60 dimensions
Const MAX_DIMENSION As Long = 60
If numberOfDimensions < 1 Or numberOfDimensions > MAX_DIMENSION Then
Err.Raise 5, "EmptyArray", "Invalid number of dimensions"
End If
#If Mac Then
Err.Raise 298, "EmptyArray", "OleAut32.dll required"
#Else
Dim bounds() As SAFEARRAYBOUND
#If VBA7 Then
Dim ptrArray As LongPtr
#Else
Dim ptrArray As Long
#End If
Dim tVariant As tagVariant
Dim i As Long
'
ReDim bounds(0 To numberOfDimensions - 1)
'
'Make lower dimensions [0 to 0] instead of [0 to -1]
For i = 1 To numberOfDimensions - 1
bounds(i).cElements = 1
Next i
'
'Create empty array and store pointer
ptrArray = SafeArrayCreate(vType, numberOfDimensions, bounds(0))
'
'Create a Variant pointing to the array
tVariant.vt = vbArray + vType
tVariant.ptr = ptrArray
'
'Copy result
VariantCopy EmptyArray, tVariant
'
'Clean-up
SafeArrayDestroy ptrArray
#End If
End Function
You can now create empty arrays with different number of dimensions and data types:
Sub Test()
Dim arr2D() As Variant
Dim arr4D() As Double
'
arr2D = EmptyArray(2, vbVariant)
arr4D = EmptyArray(4, vbDouble)
Stop
End Sub
Update 30/09/2022
I've created an EmptyArray method (same signature) in my MemoryTools library on GitHub. That version will work on both Windows and Mac.
Idk man - I think you stumbling onto this property was pretty wild.
I'd probably stop here and just do:
Function Empty2DArray() As Variant
With CreateObject("Forms.ComboBox.1")
.List = Array()
Empty2DArray = .List
End With
End Function
And use it like: a = Empty2DArray
You don't need to create the userform or combobox - you can just use CreateObject.
But as others have said, it probably makes more sense to do error handling when checking whether or not your arrays are initialized.
I have had a look at this StackOverflow article and the same thing applies to me. Why is it that RUNDLL32.EXE user32.dll,UpdatePerUserSystemParameters 1, True does not work everytime? Is there some other way to make it work rather than repeating that until it works or is there some way to code it so that it works? .cmd , .bat and .ps1 is fine) Or is the best/only way to run it alot of times so that it works
Right now my solution is to just run that multiple time until it works. Is there any other way to refresh the desktop wallpaper without running RUNDLL32.EXE user32.dll,UpdatePerUserSystemParameters 1, True alot of times?
From Help
https://learn.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-systemparametersinfow
Although this is from the 2001 documentation and has been removed from current.
Setting pvParam to "" removes the wallpaper. Setting pvParam to VBNULL
reverts to the default wallpaper.
REM ChangeWallpaper.bat
REM Compiles ChangeWallpaper.vb to ChangeWallpaper.exe
C:\Windows\Microsoft.NET\Framework\v4.0.30319\vbc "%~dp0\ChangeWallpaper.vb" /out:"%~dp0\ChangeWallpaper.exe" /target:winexe
pause
;ChangeWallpaper.vb
Imports System.Runtime.InteropServices
Public Module ChangeWallpaper
Public Declare Unicode Function SystemParametersInfoW Lib "user32" (ByVal uAction As Integer, ByVal uParam As Integer, ByVal lpvParam As String, ByVal fuWinIni As Integer) As Integer
Public Const SPI_SETDESKWALLPAPER = 20
Public Const SPIF_SENDWININICHANGE = &H2
Public Const SPIF_UPDATEINIFILE = &H1
Public Sub Main()
Dim Ret as Integer
Dim FName As String
Fname = "C:\Windows\Web\Wallpaper\Theme1\img1.jpg"
'This below line which is commented out takes a filename on the command line
'FName = Replace(Command(), """", "")
Ret = SystemParametersInfoW(SPI_SETDESKWALLPAPER, 0, FName, SPIF_SENDWININICHANGE + SPIF_UPDATEINIFILE)
If Ret = 0 Then Msgbox(err.lastdllerror)
End Sub
End Module
The code is from here https://winsourcecode.blogspot.com/2019/06/changewallpaper.html
Update
This is the problem with using it
Declare Function UpdatePerUserSystemParameters Lib "User32.dll" (ByVal i As Long, ByVal b As Boolean) As long
As you can see from the article Rundll32 is passing a hwnd (probably 0 to say Desktop is the parent) for j and RunDll32's HInst as a Boolean for b, and as this will be non zero it will be treated as true.
I want to run a batch program(.bat) through a Visual Basic 6.0 application and also want to print the output of the batch program(.bat) in the Visual Basic 6.0 application. I want to execute the dir command in the batch file so that VB6.0 application can print the output in a text box.
VB6.0 code:
Dim com As String
Dim wshThisShell
Dim lngRet As Long
Dim strShellCommand As String
Dim strBatchPath As String
Sub C0ding()
Set wshThisShell = CreateObject("WScript.Shell")
strBatchPath = "C:\first.bat"
strShellCommand = """" & strBatchPath & """"
lngRet = wshThisShell.Run(strShellCommand, vbNormalFocus, vbTrue)
End Sub
Private Sub Command1_Click()
C0ding
End Sub
first.bat:
dir c:\
In the above example 'first.bat' is batch file and containing the 'dir c:\' command. Now VB6.0 app will run the first.bat and show the output of the 'dir c:\' command in a text box.
Please also tell me that I can achieve this requirement means can VB6.0 application regain the control from batch program(.bat)?
Please help me with this.
Following is solution which worked for me:
Private Declare Function CreatePipe Lib "kernel32" (phReadPipe As Long, phWritePipe As Long, lpPipeAttributes As Any, ByVal nSize As Long) As Long
Private Declare Function ReadFile Lib "kernel32" (ByVal hFile As Long, ByVal lpBuffer As String, ByVal nNumberOfBytesToRead As Long, lpNumberOfBytesRead As Long, ByVal lpOverlapped As Any) As Long
Private Declare Function GetNamedPipeInfo Lib "kernel32" (ByVal hNamedPipe As Long, lType As Long, lLenOutBuf As Long, lLenInBuf As Long, lMaxInstances As Long) As Long
Private Type SECURITY_ATTRIBUTES
nLength As Long
lpSecurityDescriptor As Long
bInheritHandle As Long
End Type
Private Type STARTUPINFO
cb As Long
lpReserved As Long
lpDesktop As Long
lpTitle As Long
dwX As Long
dwY As Long
dwXSize As Long
dwYSize As Long
dwXCountChars As Long
dwYCountChars As Long
dwFillAttribute As Long
dwFlags As Long
wShowWindow As Integer
cbReserved2 As Integer
lpReserved2 As Long
hStdInput As Long
hStdOutput As Long
hStdError As Long
End Type
Private Type PROCESS_INFORMATION
hProcess As Long
hThread As Long
dwProcessID As Long
dwThreadID As Long
End Type
Private Declare Function WaitForSingleObject Lib "kernel32" (ByVal hHandle As Long, ByVal dwMilliseconds As Long) As Long
Private Declare Function CreateProcessA Lib "kernel32" (ByVal lpApplicationName As Long, ByVal lpCommandLine As String, lpProcessAttributes As Any, lpThreadAttributes As Any, ByVal bInheritHandles As Long, ByVal dwCreationFlags As Long, ByVal lpEnvironment As Long, ByVal lpCurrentDirectory As Long, lpStartupInfo As Any, lpProcessInformation As Any) As Long
Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
'Purpose : Synchronously runs a DOS command line and returns the captured screen output.
'Inputs : sCommandLine The DOS command line to run.
' [bShowWindow] If True displays the DOS output window.
'Outputs : Returns the screen output
'Notes : This routine will work only with those program that send their output to
' the standard output device (stdout).
' Windows NT ONLY.
'Revisions :
Function ShellExecuteCapture(sCommandLine As String, Optional bShowWindow As Boolean = False) As String
Const clReadBytes As Long = 256, INFINITE As Long = &HFFFFFFFF
Const STARTF_USESHOWWINDOW = &H1, STARTF_USESTDHANDLES = &H100&
Const SW_HIDE = 0, SW_NORMAL = 1
Const NORMAL_PRIORITY_CLASS = &H20&
Const PIPE_CLIENT_END = &H0 'The handle refers to the client end of a named pipe instance. This is the default.
Const PIPE_SERVER_END = &H1 'The handle refers to the server end of a named pipe instance. If this value is not specified, the handle refers to the client end of a named pipe instance.
Const PIPE_TYPE_BYTE = &H0 'The named pipe is a byte pipe. This is the default.
Const PIPE_TYPE_MESSAGE = &H4 'The named pipe is a message pipe. If this value is not specified, the pipe is a byte pipe
Dim tProcInfo As PROCESS_INFORMATION, lRetVal As Long, lSuccess As Long
Dim tStartupInf As STARTUPINFO
Dim tSecurAttrib As SECURITY_ATTRIBUTES, lhwndReadPipe As Long, lhwndWritePipe As Long
Dim lBytesRead As Long, sBuffer As String
Dim lPipeOutLen As Long, lPipeInLen As Long, lMaxInst As Long
tSecurAttrib.nLength = Len(tSecurAttrib)
tSecurAttrib.bInheritHandle = 1&
tSecurAttrib.lpSecurityDescriptor = 0&
lRetVal = CreatePipe(lhwndReadPipe, lhwndWritePipe, tSecurAttrib, 0)
If lRetVal = 0 Then
'CreatePipe failed
Exit Function
End If
tStartupInf.cb = Len(tStartupInf)
tStartupInf.dwFlags = STARTF_USESTDHANDLES Or STARTF_USESHOWWINDOW
tStartupInf.hStdOutput = lhwndWritePipe
If bShowWindow Then
'Show the DOS window
tStartupInf.wShowWindow = SW_NORMAL
Else
'Hide the DOS window
tStartupInf.wShowWindow = SW_HIDE
End If
lRetVal = CreateProcessA(0&, sCommandLine, tSecurAttrib, tSecurAttrib, 1&, NORMAL_PRIORITY_CLASS, 0&, 0&, tStartupInf, tProcInfo)
If lRetVal <> 1 Then
'CreateProcess failed
Exit Function
End If
'Process created, wait for completion. Note, this will cause your application
'to hang indefinately until this process completes.
WaitForSingleObject tProcInfo.hProcess, INFINITE
'Determine pipes contents
lSuccess = GetNamedPipeInfo(lhwndReadPipe, PIPE_TYPE_BYTE, lPipeOutLen, lPipeInLen, lMaxInst)
If lSuccess Then
'Got pipe info, create buffer
sBuffer = String(lPipeOutLen, 0)
'Read Output Pipe
lSuccess = ReadFile(lhwndReadPipe, sBuffer, lPipeOutLen, lBytesRead, 0&)
If lSuccess = 1 Then
'Pipe read successfully
ShellExecuteCapture = Left$(sBuffer, lBytesRead)
End If
End If
'Close handles
Call CloseHandle(tProcInfo.hProcess)
Call CloseHandle(tProcInfo.hThread)
Call CloseHandle(lhwndReadPipe)
Call CloseHandle(lhwndWritePipe)
End Function
Sub Test()
'Debug.Print ShellExecuteCapture("C:\first.bat", False)
Text1.Text = ShellExecuteCapture("C:\first.bat", False)
End Sub
Private Sub Command1_Click()
Call Test
End Sub
I got this solution from the following link:
Solution Link
Your example is not a batch file, but if all you want to do is display the results of a command prompt's dir c:\ command in a textbox, then the following should work:
Disclaimer: The following is "Air Code" and not tested for syntax
Private Sub Command1_Click()
Dim sCommand As String
sCommand = "dir c:\ > C:\tempFile.txt"
Shell "%COMSPEC% /c " & sCommand
Dim inCh As Integer
inCh = Freefile
Open "C:\tempFile.txt" For Input As inCh
Text1.Text = Input$(Lof(inCh), inCh)
Close inCh
End Sub
There are several variations and alternative ways to accomplish this, this is just a quick-and-dirty solution example.
Lots of simple ways to skin this cat, for example:
Option Explicit
'Reference to: Windows Script Host Object Model
Private WshExec As IWshRuntimeLibrary.WshExec
Private Sub Form_Load()
With New IWshRuntimeLibrary.WshShell
Set WshExec = .Exec("cmd.exe /c dir c:\")
End With
Timer1.Interval = 100
End Sub
Private Sub Form_Resize()
If WindowState <> vbMinimized Then
Text1.Move 0, 0, ScaleWidth, ScaleHeight
End If
End Sub
Private Sub Timer1_Timer()
With WshExec
Select Case .Status
Case WshFinished, WshFailed
Text1.Text = .StdOut.ReadAll()
Timer1.Interval = 0
End Select
End With
End Sub
I am trying to add compressed bitmap as resource of another executable, but got stuck to an error. The error is:
Value of type 'System.Drawing.Bitmap' cannot be converted to '1-dimensional array of System.Drawing.Bitmap'
Here's my pseudo code:
Module1:
Imports System.Runtime.InteropServices
Module ResourceWriter
Private Function ToPtr(ByVal data As Object) As IntPtr
Dim h As GCHandle = GCHandle.Alloc(data, GCHandleType.Pinned)
Dim ptr As IntPtr
Try
ptr = h.AddrOfPinnedObject()
Finally
h.Free()
End Try
Return ptr
End Function
<DllImport("kernel32.dll", SetLastError:=True)> _
Private Function UpdateResource(ByVal hUpdate As IntPtr, ByVal lpType As String, ByVal lpName As String, ByVal wLanguage As UShort, ByVal lpData As IntPtr, ByVal cbData As UInteger) As Boolean
End Function
<DllImport("kernel32.dll", SetLastError:=True)> _
Private Function BeginUpdateResource(ByVal pFileName As String, <MarshalAs(UnmanagedType.Bool)> ByVal bDeleteExistingResources As Boolean) As IntPtr
End Function
<DllImport("kernel32.dll", SetLastError:=True)> _
Private Function EndUpdateResource(ByVal hUpdate As IntPtr, ByVal fDiscard As Boolean) As Boolean
End Function
Public Function WriteResource(ByVal filename As String, ByVal bmp As Bitmap()) As Boolean
Try
Dim handle As IntPtr = BeginUpdateResource(filename, False)
Dim file1 As Bitmap() = bmp
Dim fileptr As IntPtr = ToPtr(file1)
Dim res As Boolean = UpdateResource(handle, "BitMaps", "0", 0, fileptr, Convert.ToUInt32(file1.Length))
EndUpdateResource(handle, False)
Catch ex As Exception
Return False
End Try
Return True
End Function
End Module
In form, under button:
'...here's code to compress the image, commented out for now
Dim bmp1 As Bitmap = Compressed
WriteResource("C:\Users\Admin\Desktop\Testfile.exe", bmp1)
But it doesn't work. What changes I should make to the module, or to the code under button? I see I should convert System.Drawing.Bitmap to 1-dimensional array before putting the image into the resources, but how?
Any help is much appreciated :)
Edit:
I have now tried all answers I found from google & MSDN, and I cannot figure it out. So if anyone could just show how to do it, I would really appreciate it..
Here's one of the methods I tried.
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
'...
Dim bmp1 As Bitmap = Compressed
Dim Converted = ConvertToByteArray(bmp1)
WriteResource("C:\Users\Admin\Desktop\Testfile.exe", Converted)
End Sub
Public Shared Function ConvertToByteArray(ByVal value As Bitmap) As Byte()
Dim bitmapBytes As Byte()
Using stream As New System.IO.MemoryStream
value.Save(stream, value.RawFormat)
bitmapBytes = stream.ToArray
End Using
Return bitmapBytes
End Function
And yes, I changed the Bitmap() to Byte() at Module1; but it returned "Value cannot be NULL" in runtime.
I also tried to save it as IO.MemoryStream and then convert to bytes but didn't success.
So if anyone could show me how to do this, that would be really great.
You declared the parameter as a Bitmap array by putting () after the type name here:
Public Function WriteResource(ByVal filename As String, ByVal bmp As Bitmap()) As Boolean
If you don't want it to be an array, remove the ():
Public Function WriteResource(ByVal filename As String, ByVal bmp As Bitmap) As Boolean
The first problem you have is well covered in Ryan's answer (Dim file1 As Bitmap() = bmp is wrong too); the second is that you are covering up a different problem.
If you refer to UpdateResource on MSDN you'll see that cbdata is the number of bytes to write, that is the byte count of the bitmap. Your code is passing the size of the array. Further, lpData is supposed to be a long pointer to the data and also "Note that this is the raw binary data to be stored". You cannot just pass a bitmap as you are trying to do.
The bitmap class's save method will let you save to a memorystream from which the bytes AND BYTE COUNT can be gotten and fed to UpdateResource.
Ok, so I have these functions I'm tring to use via my vba code.
It's probably the as it would have been with vbs as well.
Here's the function(s)
'declarations for working with Ini files
Private Declare Function GetPrivateProfileSection Lib "kernel32" Alias _
"GetPrivateProfileSectionA" (ByVal lpAppName As String, ByVal lpReturnedString As String, _
ByVal nSize As Long, ByVal lpFileName As String) As Long
Private Declare Function GetPrivateProfileString Lib "kernel32" Alias _
"GetPrivateProfileStringA" (ByVal lpApplicationName As String, ByVal lpKeyName As Any, _
ByVal lpDefault As String, ByVal lpReturnedString As String, ByVal nSize As Long, _
ByVal lpFileName As String) As Long
'// INI CONTROLLING PROCEDURES
'reads an Ini string
Public Function ReadIni(Filename As String, Section As String, Key As String) As String
Dim RetVal As String * 255, v As Long
v = GetPrivateProfileString(Section, Key, "", RetVal, 255, Filename)
ReadIni = Left(RetVal, v + 0)
End Function
'reads an Ini section
Public Function ReadIniSection(Filename As String, Section As String) As String
Dim RetVal As String * 255, v As Long
v = GetPrivateProfileSection(Section, RetVal, 255, Filename)
ReadIniSection = Left(RetVal, v + 0)
End Function
How can I use this to create a function that basically allows me to specify only the section I want to look in, and then find each ini string within that section and put it into an array and return that Array so I can do a loop with it?
Edit: I see that ReadIniSection returns all of the keys in a huge string.
Meaning, I need to split it up.
ReadIniSection returns something that looks like this:
"Fornavn=FORNAVN[]Etternavn=ETTERNAVN" etc etc. The[] in the middle there isn't brackets, it's a square. Probably some character it doesn't recognize. So I guess I should run it through a split command that takes the value between a = and the square.
See if this helps - splitting on nullchar \0:
Private Sub ListIniSectionLines()
Dim S As String: S = ReadIniSection("c:\windows\win.ini", "MAIL")
Dim vLines As Variant: vLines = Split(S, Chr$(0))
Dim vLine As Variant
For Each vLine In vLines
Debug.Print vLine
Next vLine
End Sub