8

I am developing an application which opens and reads an XML document previously embedded in a PowerPoint presentation, or a Word document. In order to read this object (xmlFile as Object) I have to do:

xmlFile.OLEFormat.DoVerb 1

This opens the package object, and I have another subroutine that gets the open instance of Notepad.exe, and reads its contents in to ADODB stream.

An example of this procedure is available on Google Docs:

XML_Test.pptm.

During this process there is a few seconds window where the Notepad.exe gains focus, and an inadvertent keystroke may cause undesired results or error reading the XML data.

I am looking for one of two things:

  1. Either a method to prevent the user from inadvertently inputting (via keyboard/mouse/etc) while this operation is being performed. Preferably something that does not take control of the user's machine like MouseKeyboardTest subroutine, below. Or,
  2. A better method of extracting the XML data into a string variable.

For #1: this is the function that I found, which I am leery of using. I am wary of taking this sort of control of the users system. ##Are there any other methods that I might use?##

Private Declare Function BlockInput Lib "USER32.dll" (ByVal fBlockIt As Long) As Long
Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

Sub MouseKeyboardTest() 'both keyboard and mouse blocked

    BlockInput True ' Turns off Keyboard and Mouse
'   Routine goes here
    Sleep 5000 ' Optional coding
    BlockInput False ' Turns on Keyboard and Mouse

End Sub

For #2: Some background, but the issue seems to be the inability to extract the embedded object reliably using any method other than DoVerb 1. Since I am dealing with an unsaved document in an application (Notepad) that is immune to my VBA skillz, this seems to be the only way to do this. Full background on that, here:

Extracting an OLEObject (XML Document) from PowerPoint VBA

Community
  • 1
  • 1
David Zemens
  • 53,033
  • 11
  • 81
  • 130
  • Can you not just read your xml using a [`FileSystemObject`](http://msdn.microsoft.com/en-us/library/office/gg278516.aspx) and [`TextStream`](http://msdn.microsoft.com/en-us/library/aa242724(v=vs.60).aspx)? –  May 07 '13 at 06:56
  • @Mehow The problem is extracting the embedded object from the PPT document. If I can extract the file reliably, then I could probably use FSO. The only method I have found to extract an embedded Package Object is `DoVerb 1`. Background info: http://stackoverflow.com/questions/14745056/extracting-an-oleobject-xml-document-from-powerpoint-vba – David Zemens May 07 '13 at 11:54
  • I tested the function you posted - it does work but still the keystrokes are displayed even though they are being 'blocked' while the function is running. Its not the worst function to use but i can see your point. Have you thought of maybe hooking up a listener instead of blocking keyboard and redirecting keystrokes away from notepad? [Check this out](http://stackoverflow.com/questions/15038413/how-to-listen-keyboard-in-background-and-fire-keystrokes-on-demand) –  May 07 '13 at 13:45
  • @mehow I think that option may be above my skill set at the moment. I added an example PPTM file to the question which will show how I am currently extracting and writing the XML. – David Zemens May 07 '13 at 14:08
  • I have an idea(Untested). If the text file is minimized automatically then would that solve your problem? – Siddharth Rout May 07 '13 at 21:25
  • Let me know if that will help so that I can post the code. – Siddharth Rout May 07 '13 at 21:48
  • @SiddharthRout Yes, I think if I could take focus away from Notepad (so that an errant keystroke would not affect its contents), while still being able to read its contents to the ADODB stream, I think that would be a good solution. Please do post your code, I think I am close to figuring this out, it will be good to compare your code to mine. – David Zemens May 07 '13 at 21:50
  • You will certainly have it finished before I do. I will be heading home shortly, and I will resume my attempt later tonight, when I will review yours as well. Thanks! – David Zemens May 07 '13 at 21:53
  • @SiddharthRout I think I can modify [this](http://www.mrexcel.com/forum/excel-questions/341441-visual-basic-applications-code-minimize-window.html). – David Zemens May 07 '13 at 21:54
  • My approach is similar but slightly different. let me know if you still want me to post the code... I am about to press the submit button... LOL – Siddharth Rout May 07 '13 at 21:55
  • 1
    I anyways posted the solution... And not for bounty BTW :) + 1 BTW for a nice question.... – Siddharth Rout May 07 '13 at 22:00
  • @SiddharthRout I have just figured it out :) Time to go home, but I will definitely review yours as well. – David Zemens May 07 '13 at 22:01

4 Answers4

2

As you correctly guessed in the comment above that taking the focus away from notepad will solve your problem. The below code does exactly that.

LOGIC:

A. Loop through the shape and get it's name. In your scenario it would be something like Chart Meta XML_fbc9775a-19ea-.txt

enter image description here

B. Use APIs like FindWindow, GetWindowTextLength, GetWindow etc to get the handle of the notepad window using partial caption.

C. Use the ShowWindow API to minimize the window

Code (tested in VBA-Powerpoint)

Paste this code in a module in the above PPTM

Private Declare Function FindWindow Lib "User32" Alias "FindWindowA" _
(ByVal lpClassName As String, ByVal lpWindowName As String) As Long

Private Declare Function GetWindowText Lib "User32" Alias "GetWindowTextA" _
(ByVal hWnd As Long, ByVal lpString As String, ByVal cch As Long) As Long

Private Declare Function GetWindowTextLength Lib "User32" Alias _
"GetWindowTextLengthA" (ByVal hWnd As Long) As Long

Private Declare Function GetWindow Lib "User32" (ByVal hWnd As Long, _
ByVal wCmd As Long) As Long

Private Declare Function ShowWindow Lib "User32" (ByVal hWnd As Long, _
ByVal nCmdShow As Long) As Long

Private Const GW_HWNDNEXT = 2
Private Const SW_SHOWMINIMIZED = 2

Sub Sample()
    Dim shp As Shape
    Dim winName As String
    Dim Ret As Long

    For Each shp In ActivePresentation.Slides(1).Shapes
        If shp.Type = msoEmbeddedOLEObject Then
            winName = shp.Name
            shp.OLEFormat.Activate
            Exit For
        End If
    Next

    If winName <> "" Then
        Wait 1

        If GetHwndFromCaption(Ret, Replace(winName, ".txt", "")) = True Then
           Call ShowWindow(Ret, SW_SHOWMINIMIZED)
        Else
            MsgBox "Window not found!", vbOKOnly + vbExclamation
        End If
    End If
End Sub

Private Function GetHwndFromCaption(ByRef lWnd As Long, ByVal sCaption As String) As Boolean
    Dim Ret As Long
    Dim sStr As String

    GetHwndFromCaption = False

    Ret = FindWindow(vbNullString, vbNullString)

    Do While Ret <> 0

        sStr = String(GetWindowTextLength(Ret) + 1, Chr$(0))
        GetWindowText Ret, sStr, Len(sStr)
        sStr = Left$(sStr, Len(sStr) - 1)
        If InStr(1, sStr, sCaption) > 0 Then
            GetHwndFromCaption = True
            lWnd = Ret
            Exit Do
        End If
        Ret = GetWindow(Ret, GW_HWNDNEXT)
    Loop
End Function

Private Sub Wait(ByVal nSec As Long)
    nSec = nSec + Timer
    While nSec > Timer
        DoEvents
    Wend
End Sub
Siddharth Rout
  • 147,039
  • 17
  • 206
  • 250
  • Great answer and adequately addresses the question, however, I think for some of the reasons Ilya mentiosn below, keeping the XML content in the document, rather than an OLEObject, is the best approach. Cheers! – David Zemens May 10 '13 at 01:11
1

My understanding is that you have control over how XML file gets embedded into PowerPoint presentation in the first place. Here I do not quite understand why you chose to keep the data you need as contents of an embedded object.

To be sure, the task of getting those contents back is not a piece of cake. Actually, as long as there is no (simple or even moderately difficult) way to call QueryInterface and use IPersist* interfaces from VBA, there is just one way to get to contents of embedded object. The way involves following steps:

  1. Activate an embedded object. You used OLEFormat.DoVerb 1 for that. A better way would be to call OLEFormat.Activate, but this is irrelevant for your particular problem.
  2. Use embedded object's programming model to perform useful operations like getting contents, saving or whatever is exposed. Notepad.exe exposes no such programming model, and you resorted to WinAPI which is the best choice available.

Unfortunately, your current approach has at least 2 flaws:

  1. The one you identified in the question (activation of notepad.exe leading to possibility of user's interference).
  2. If a user has default program for opening .txt files other than notepad.exe, your approach is doomed.

If you do have control over how embedded object is created then better approach would be to store your XML data in some property of Shape object. I would use Shape.AlternativeText (very straightforward to use; shouldn't be used if you export your .pptm to HTML or have some different scenario where AlternativeText matters) or Shape.Tags (this one is probably the most semantically correct for the task) for that.

Ilya Kurnosov
  • 3,180
  • 3
  • 23
  • 37
  • Thanks for the detailed response. I have control over how the XML gets embedded. I am writing a txt/xml file with FSO, then embedding that file as the object. I had experimented with storing the contents in a `TextBox` shape, but had some problems with the application crashing when I try this with very long strings (several hundred thousands of characters). I am going to revisit that approach and/or the `AlternativeText` property, since I think the crash error was caused by how I was generating "dummy" string data (a loop caused the crash, not the size of the string). – David Zemens May 08 '13 at 00:07
  • 1
    There is a limit to this approach somewhere about 5.23MM and 5.28MM characters but that may well exceed the length of any user-generated XML (that is about 100x larger than the sample XML embed). I will have to do further testing to make sure this limit is within reasonably expected use cases, but if not, I could always split the XML string in to multiple shapes. I very much prefer this method so far. Thanks again! – David Zemens May 08 '13 at 00:40
  • This is working great -- and it's pretty much what I *wanted* to do before I went down the XML/Notepad route, anyways. Although Sid's response also adequately resolves this problem, I think this approach is ultimately the best solution for the reasons you stated. – David Zemens May 10 '13 at 01:09
0

I don't think that blocking the user is the right approach,

If you must use a content of a notepad window, I would suggest using the SendKeys method, in order to send this combination:

SendKeys("^A^C")

Which is the equivalent of "Select All" and "Copy",

And then you could continue working "offline" on the clipboard, without fear of interference by keystrokes.

Uri Goren
  • 13,386
  • 6
  • 58
  • 110
  • I can get the Notepad, it takes only 1-3 seconds to get the Notepad window and read the contents in to ADODB stream. This stream is then passed to a COM object, but during that few seconds, the user can inadvertently interrupt the process as Notepad has focus. I am willing to try `SendKeys` if it's the only option, but I am under the impression that this method is generally frowned upon and should be avoided. Then again, so is blocking user input from peripherals :) – David Zemens May 07 '13 at 02:01
  • Thanks Uri. Blocking the user is probably not the right approach. I was not able to implement `SendKeys` method in previous attempts, and could not get that to work this time around, either. In any case, I would've still preferred to avoid using this method and to that end both Ilya and Sid provided some alternatives approaches which I was able to implement. Cheers! – David Zemens May 10 '13 at 02:34
0

My approach, per Sid's suggestion, was to find a way to minimize the Notepad.exe. Since I already found way to get that object and close it, I figured this should not be as hard.

I add these:

Public Declare Function _
     ShowWindow& Lib "user32" (ByVal hwnd As Long, _
                        ByVal ncmdshow As Long)
Public Const SW_MINIMIZE = 6

And then, in the FindNotepad function, right before Exit Function (so, after the Notepad has been found) I minimize the window with:

ShowWindow TopWnd, SW_MINIMIZE
David Zemens
  • 53,033
  • 11
  • 81
  • 130
  • 1
    The challenge is how will you find the notepad window if there are say 7 notepad windows opened ;) And hence the code that I posted which gets the handle of the window using partial caption. – Siddharth Rout May 07 '13 at 22:01
  • @SiddharthRout the code I am using already accounts for the filename, however thanks tot his comment I notice that the `CloseApp_B` function which terminates the process, does not account for this, and might inadvertently close other instance of notepad (if any). I will look at your code a little more closely when I get home this evening. – David Zemens May 07 '13 at 22:04