.NET Forum / .NET Framework / Interop / September 2004
Windows Service to Populate Text Boxes in Pre-Existing Adobe PDF
|
|
Thread rating:  |
Geoff - 27 Sep 2004 18:41 GMT I succeeded at writing a Windows Forms app to write values into text fields on a PDF, save the PDF and email or fax the PDF to customers, and then the server team asked me to turn the program into a Windows Service.
I'm having a couple of problems related to removing windows.forms from the project. The problem I can't resolve or work around is that the Adobe Acrobat API may throw message boxes during processing. I handled this in my windows.form app by enumerating all open windows looking for a particular window title and then sending an enter key to the window using SendKeys from system.windows.forms.
So, in trying to convert the program, I realize that I will not be able to use this method (can't compile). I'm guessing that the API trying to throw a window may cause the service to hang anyway, although I don't know. I'll test that next.
Other problems I have had caused by removing the windows.forms is that I cannot use invoke to return to my original thread. I also can't bind a FileSystemWatcher to the main thread of the app. These problems I can get around.
I hope that there is a way to succeed at making this program a service. Does anyone have any ideas?
Thanks,
Geoff
gswitz - 28 Sep 2004 14:43 GMT I've gotten the app to run as a service. In the OnStart I create a loop and use thread.sleep to get the app to run on just one thread (the adobe api can't handle multiple threads). This works, but causes the service to show as starting (not started) in the service viewer area of CompManagement in the MMC. I'm not worried about this.
Code for OnStart... Dim i As Integer = 5000 'Length of time to sleep the thread while waiting for the next thing to come along... Dim f As FileInfo Dim d As New DirectoryInfo("c:\Pdf Populator Service\InBox")
Do Until blnStop = True For Each f In d.GetFiles() If f.Extension = ".xml" Then Timer_KillAdobeWindow.Start() MakeCallToProcessFiles() FinishProcessing(True) Timer_KillAdobeWindow.Stop() End If Next Thread.Sleep(i) Loop
As long as all the parameters for the text boxes in the PDF meet the PDF requirements, the program will populate the PDF, save it and email it out (or fax it). The problem comes if bad data is passed to the program and Adobe will not accept it (example: a date value with 2 digit year). When this occurs when the app is running as a windows form a MessageBox is thrown from Adobe. I use my WindowsCloser Class on a timer to enumerate open windows looking for such a window and send an enter key to the window using System.Windows.Forms.SendKeys. On the Windows Forms version this works, but with the Windows Service version of the app, things keep hanging up. I try to attach to the process to debug, but don't seem to get anything (perhaps because the program is waiting on a return from Adobe?). Sometimes, using taskmanager to endprocess on the Adobe app will suddenly throw an exception in the .Net Service App.
For the timer, I'm using Friend WithEvents Timer_KillAdobeWindow As System.Timers.Timer
My Windows Closer Class called from the elapsed event on the async timer is ... Option Explicit On Option Strict Off
Imports System Imports System.IO Imports System.Text Imports System.Runtime.InteropServices Imports System.Threading
Public Delegate Function CallBack(ByVal handle As Integer, ByVal param As IntPtr) As Boolean
Public Class LibWrap ' Passes a managed object instead of an LPARAM. ' Declares a managed prototype for the unmanaged function. Declare Function EnumWindows Lib "user32.dll" ( _ ByVal cb As CallBack, ByVal param As IntPtr) As Boolean End Class 'LibWrap
Public Class WindowCloser Shared cstrPartialTitle As String
Public Shared Sub GetWindowFromTitle() Dim tw As TextWriter = System.Console.Out Dim gch As GCHandle = GCHandle.Alloc(tw) ' Platform invoke prevents the delegate from being garbage collected ' before the call ends. Dim cewp As CallBack cewp = AddressOf WindowCloser.CaptureEnumWindowsProc LibWrap.EnumWindows(cewp, GCHandle.op_Explicit(gch)) gch.Free() End Sub 'GetWindowFromTitle Public Shared Function CaptureEnumWindowsProc(ByVal handle _ As Integer, ByVal param As IntPtr) As Boolean Try Dim gch As GCHandle = GCHandle.op_Explicit(param) 'Dim tw As TextWriter = CType(gch.Target, TextWriter) Dim w As New Window Try w.h = handle If w.GetText = PartialTitle Then AppActivate(PartialTitle) System.Windows.Forms.SendKeys.Send("{Enter}") End If Return True Catch ex As Exception 'do nothing Finally gch = Nothing w = Nothing End Try Catch ex As Exception 'do nothing End Try End Function 'CaptureEnumWindowsProc Public Shared Property PartialTitle() As String Get PartialTitle = cstrPartialTitle End Get Set(ByVal Value As String) cstrPartialTitle = Value End Set End Property End Class Public Class Win32API Public Declare Auto Sub GetWindowText Lib "User32.Dll" _ (ByVal h As Integer, ByVal s As StringBuilder, ByVal nMaxCount As Integer) End Class Public Class Window Friend h As Integer ' Friend handle to Window. Public Function GetText() As String Dim sb As New StringBuilder(256) Win32API.GetWindowText(h, sb, sb.Capacity + 1) Return sb.ToString() End Function End Class
gswitz - 28 Sep 2004 15:05 GMT In my WindowCloser Class, in CaptureEnumWindowsProc, the line... AppActivate(PartialTitle) is throwing an exception when it finds the window with "Adobe Acrobat" in the title and tries to activate that window.
Any ideas as to how I can send an enter key to this Window? I have the Window Handle.
I know I'm soo close to making this work! Major thanks to anyone who can nudge me over the top!
Thanks,
Geoff
gswitz - 28 Sep 2004 21:27 GMT I've tried using unmanaged code... Private Declare Function SetForegroundWindow Lib "user32" (ByVal hwnd As Long) As Long ' Declare the Windows API function CopyMemory ' Note that all variables are ByVal. pDst is passed ByVal because we want ' CopyMemory to go to that location and modify the data that is pointed to ' by the IntPtr, and not the pointer itself. Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByVal pDst As IntPtr, _ ByVal pSrc As String, _ ByVal ByteLen As Long)
I can get the SetForegroundWindow call to work (or not throw an error), but I cannot get the CopyMemory to work. My original source code that I find to try to send keys to the window was in VB 6 and used the ANY type which is not supported in .Net. My team leader tells me I should bag it, write the window closer in VB 6 and shell out to it from .Net. Is this really the best way to get this thing to work? I'm really disappointed.
Mattias Sj?gren - 29 Sep 2004 00:57 GMT >I can get the SetForegroundWindow call to work (or not throw an error) But it isn't correct. The parameter should be an IntPtr and the return type Boolean.
>but I cannot get the CopyMemory to work. The last parameter here should be an Integer, not Long.
Mattias
 Signature Mattias Sjögren [MVP] mattias @ mvps.org http://www.msjogren.net/dotnet/ | http://www.dotnetinterop.com Please reply only to the newsgroup.
gswitz - 29 Sep 2004 13:29 GMT These tips really haven't helped me get where I need to go, even if you are correct. I've found lots of conflicting examples for the parameter values for the api calls.
Regardless, I only need to manage to send keys to the application...
In the example below, the user is able to copymemory and sendinput to another window. This is what I need to do. I need to send an enter key down and up to another application. The fact that I cannot use types in .net (Structures cannot be passed in the same manner the types could be passed to the api in vb6) and that there are no examples of how to do this nor can windows.forms.sendkeys be used in a windows service means I'm stuck.
Mattias, if you can help me, I would be greatly appreciative. I am now beginning to build the vb app to do the job and shell out to the vb app from .net b/c I can't figure out how to make this happen using .net after 3 days of effort.
Is there anyone out there who can help me?
Thanks,
Geoff
Private Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" (ByVal hWnd1 As Long, ByVal hWnd2 As Long, ByVal lpsz1 As String, ByVal lpsz2 As String) As Long Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long Private Declare Function SetFocus1 Lib "user32" Alias "SetFocus" (ByVal hwnd As Long) As Long Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long Private Declare Function PostMessage Lib "user32" Alias "PostMessageA" (ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long Private Declare Sub CopyMemory Lib "kernel32.dll" _ Alias "RtlMoveMemory" (Destination As Any, Source As Any, _ ByVal Length As Long) Private Type KEYBDINPUT wVk As Integer wScan As Integer dwFlags As Long time As Long dwExtraInfo As Long End Type
Private Type INPUT_TYPE dwType As Long xi(0 To 23) As Byte End Type
Private Const KEYEVENTF_KEYUP = &H2 Private Const KEYEVENTF_EXTENDEDKEY = &H1 Private Const VK_SHIFT = &H10 Private Const VK_HOME = &H24 Private Const VK_END = &H23 Private Const INPUT_KEYBOARD = 1
Private Declare Function MapVirtualKey Lib "user32" _ Alias "MapVirtualKeyA" (ByVal wCode As Long, _ ByVal wMapType As Long) As Long
Private Declare Function SendInput Lib "user32.dll" _ (ByVal nInputs As Long, pInputs As INPUT_TYPE, _ ByVal cbsize As Long) As Long
Const WM_KEYDOWN = &H100 Const WM_KEYUP = &H101 Const VK_RIGHT = &H27 Const VK_RETURN = &HD Const VK_ESCAPE = &H1B Private Sub Command1_Click() Dim wb As Excel.Workbook Dim ws As Excel.Worksheet Dim w As Excel.Window App.OleServerBusyRaiseError = True App.OleServerBusyTimeout = 1000 App.OleServerBusyMsgText = "" Set wb = WebBrowser1.Document On Error GoTo ErrorHandler Debug.Print wb.Application.ActiveWindow.Caption Exit Sub ErrorHandler: Debug.Print Err.Description Call Command3_Click wb.Application.Cells(1, 1) = "c" End Sub
Private Sub Command3_Click() Dim hwnd As Long hwnd = FindWindow("ThunderFormDC", vbNullString) hwnd = FindWindowEx(hwnd, 0, "Shell Embedding", vbNullString) SetFocus1 hwnd Dim wVkKey(1) As Integer Dim UpDown(1) As Integer wVkKey(0) = VK_ESCAPE: UpDown(0) = 0 wVkKey(1) = VK_ESCAPE: UpDown(1) = 1 sKeyEventSet 2, wVkKey, UpDown End Sub
Private Sub Form_Load() WebBrowser1.Navigate2 "file:///C:/test.xls" End Sub Private Sub sKeyEventSet(nInput As Long, _ wVkKey() As Integer, UpDown() As Integer) Dim inputevents() As INPUT_TYPE Dim keyevent As KEYBDINPUT Dim Count As Integer ReDim inputevents(nInput - 1) As INPUT_TYPE For Count = 0 To nInput - 1 With keyevent .wVk = wVkKey(Count) .wScan = MapVirtualKey(wVkKey(Count), 0) If UpDown(Count) = 0 Then .dwFlags = KEYEVENTF_EXTENDEDKEY Or 0 Else .dwFlags = KEYEVENTF_EXTENDEDKEY Or KEYEVENTF_KEYUP End If .time = 0 .dwExtraInfo = 0 End With inputevents(Count).dwType = INPUT_KEYBOARD CopyMemory inputevents(Count).xi(0), keyevent, Len(keyevent) Next Count Dim rt As Long rt = SendInput(nInput, inputevents(0), Len(inputevents(0))) Debug.Print rt Debug.Print Err.LastDllError End Sub
gswitz - 29 Sep 2004 18:39 GMT I have written the VB App to send the enter key and shelled out to it from the .net app running as a service, and this doesn't succeed at getting the Adobe Message box to go away.
I think it may be time to cut my losses.
Thanks,
Geoff
gswitz - 30 Sep 2004 16:01 GMT I opened the MMC and set the Allow service to Interact with Desktop to true on the Log On tab of the properties for the service.
After this, I was able to use... AppActivate(PartialTitle) System.Windows.Forms.SendKeys.SendWait("{Enter}") to bring the window to the front and click on it using code. The problem now is that this only works when a user is currently logged onto the computer.
I need this to work when there is no logged on user so the auditors will be happy. The auditors require that we not leave our servers logged into.
Any ideas, any one?
gswitz - 30 Sep 2004 21:49 GMT I can set the service to allow it to interact with the desktop, at which point the service can bring the Adobe Messagebox to the foreground of any currently logged in user and click the ok button. This still requires that some user be logged into the computer.
I am now testing... myprocess = Process.GetProcessesByName("Acrobat")(0) myprocess.Kill() as a method for killing the process. This doesn't really do what I want, but it will keep the service from hanging. It will prevent me from sending out a populated PDF with the email or fax, but I can still send an empty pdf with the email or fax.
I am now wondering whether it is possible to log a user in and out of the server to enable the service to click the message box using code. I don't know, but I'm curious.
Thanks,
Geoff
Free MagazinesGet these publications absolutely FREE for up to 12 months. There are no hidden fees and no obligation. Simply choose a title, complete the application form and submit it. Read more ...
|
|
|