8

I'm trying to create a macro which uses an ActiveX control button (click) to take a screenshot of my desktop screen and save it within the same excel sheet as the button. How can I create a screenshot 800x600 in size (not full desktop view) and then have it pasted into the left hand side of the same sheet as the button? I have tried this numerous ways including sendkeys (simplest).

I saved the capture process in a module:

Sub PasteScreenShot()
Application.SendKeys "({1068})"
ActiveSheet.Paste
End Sub

And then call the sub in the ActiveX button code. The capture works, but I cannot figure out a way to manipulate its area grab or its pasted location on the sheet.

I am trying to automate with buttons rather than using the snipping tool.

Teamothy
  • 2,000
  • 3
  • 16
  • 26
loco
  • 321
  • 1
  • 4
  • 18

3 Answers3

15

Without using SendKeys

Option Explicit

Private Declare Sub keybd_event Lib "user32" (ByVal bVk As Byte, ByVal _
  bScan As Byte, ByVal dwFlags As Long, ByVal dwExtraInfo As Long)

Private Const VK_SNAPSHOT = &H2C

Sub PrintScreen()
    keybd_event VK_SNAPSHOT, 1, 0, 0
    ActiveSheet.Paste
End Sub

However, with this approach if you are using multiple monitors, it will only capture the active monitor, so further effort needs to be made if you need to capture the other monitor (this can probably be done with API calls but I haven't gotten that far).

NB: The AppActivate statement can be used to activate another (non-Excel) application and if you do this, then the keybd_event function will only capture that application, e.g;

AppActivate "Windows Command Processor" 'Modify as needed
keybd_event VK_SNAPSHOT, 1, 0, 0
ActiveSheet.Paste

Using SendKeys, Problem Solved:

While SendKeys is notoriously flaky, if you need to use this method due to limiations of the API method described above, you might have some problems. As we both observed, the call to ActiveSheet.Paste was not actually pasting the Print Screen, but rather it was pasting whatever was previously in the Clipboard queue, to the effect that you needed to click your button to call the macro twice, before it would actually paste the screenshot.

I tried a few different things to no avail, but overlooked the obvious: While debugging, if I put a breakpoint on ActiveSheet.Paste, I was no longer seeing the problem described above!

enter image description here

This tells me that the SendKeys is not processed fast enough to put the data in the Clipboard before the next line of code executes, to solve that problem there are two possible solutions.

  1. You could try Application.Wait. This method seems to work when I test it, but I'd caution that it's also unreliable.
  2. A better option would be DoEvents, because it's explicitly designed to handle this sort of thing:

DoEvents passes control to the operating system. Control is returned after the operating system has finished processing the events in its queue and all keys in the SendKeys queue have been sent.

This works for me whether I run the macro manually from the IDE, from the Macros ribbon, or from a button Click event procedure:

Option Explicit
Sub CopyScreen()

Application.SendKeys "({1068})", True
DoEvents
ActiveSheet.Paste

Dim shp As Shape
With ActiveSheet
    Set shp = .Shapes(.Shapes.Count)
End With

End Sub

How To Position, Resize & Crop the Image:

Regardless of which method you use, once the picture has been pasted using ActiveSheet.Paste it will be a Shape which you can manipulate.

To Resize: once you have a handle on the shape, just assign its Height and Width properties as needed:

Dim shp As Shape
With ActiveSheet
    Set shp = .Shapes(.Shapes.Count)
End With
shp.Height = 600
shp.Width = 800

To Position It: use the shape's TopLeftCell property.

To Crop It: use the shp.PictureFormat.Crop (and/or CropLeft, CropTop, CropBottom, CropRight if you need to fine-tune what part of the screenshot is needed. For instance, this crops the pasted screenshot to 800x600:

Dim h As Single, w As Single
h = -(600 - shp.Height)
w = -(800 - shp.Width)

shp.LockAspectRatio = False
shp.PictureFormat.CropRight = w
shp.PictureFormat.CropBottom = h
David Zemens
  • 53,033
  • 11
  • 81
  • 130
  • David Zemens, I have not been able to test your solution yet. However, I failed to mention in my initial thread that it would be for a dual screen setup (thank you for reminding me). Hence, it would need to work when my excel sheet was in the right monitor and my desktop view would be in the left. Could you please advise? – loco May 11 '17 at 07:06
  • use SendKeys for both monitors, instead of keybd_event. otherwise all code remains the same – David Zemens May 11 '17 at 08:28
  • David Zemens, I apologise for my late acknowledgement of your solution. If you have time: Every time I execute the above code (using sendkeys for the left monitor screenshot), the screenshot only refreshes the content on the left monitor after the button has been clicked twice. I.e the macro is not refreshing with the new left screen content until I do two clicks (I would like a single click for a new screenshot). It captures the old content. Is there a way to reset the macro's state every time the button is clicked so that I am always given the most current information on my screen? – loco May 17 '17 at 14:19
  • Not sure I understand the problem but try adding `Application.CutCopyMode = False` at the beginning of the macro, this should clear the clipboard in Excel. If that doesn't do it, which of the two monitors is *primary*? (There is not really a "left" or "right", just a primary and secondary, which are oriented left & right) – David Zemens May 17 '17 at 14:35
  • David Zemens, "Primary" is the left monitor i.e the view which I want to capture screenshot info from. "Secondary" is right i.e the excel file view that I want the screenshot to be pasted into when I click the screenshot button. The contents of the clipboard does not refresh after each button click. It takes two button clicks for the new content in the Primary screen to be captured. I hope this is clearer. I tried your solution within the module sub at the top and also within the sheet sub I use to execute the module. Neither worked. Does this make sense? Thanks. – loco May 17 '17 at 14:50
  • Is there an application on the left monitor or are you just trying to screenshot an empty desktop? – David Zemens May 17 '17 at 14:52
  • If you're trying to capture a specific application, it might be easy. If you need the entire desktop of that monitor, maybe more difficult. – David Zemens May 17 '17 at 15:01
  • It's information on the left monitor view i.e print screens of data from other applications that are not excel. The button is quicker than using the snipping tool. I want it instead of snipping tool. The above code works perfectly and crops the primary monitor to the view frame I need (i.e cropBottom). The only issue I am having is that the code currently requires me to click the excel button twice in order to get the latest primary monitor desktop view (it pastes the old, then I click again and it pastes the new correct one). I want it to refresh and capture a new screenshot with 1 click. – loco May 17 '17 at 15:14
  • ok yes I'm sweing the same problem too. is it more than one application you're trying to copy? – David Zemens May 17 '17 at 15:19
  • Yes. The excel file is going to be open throughout the whole process. I am going to constantly be pulling up new information onto my primary view which I will then repeatedly need to print screen into my excel file. So the macro needs to always refresh to capture the latest view when the button is clicked. If this thread is too long feel free to PM me. – loco May 17 '17 at 15:22
  • I'm trying to understand whether you actually *need* to capture the ENTIRE monitor or only specific Application(s) from the monitor. – David Zemens May 17 '17 at 15:25
  • rather than APPLICATIONS I would use the term view. The macro is not interacting with any external application. It is only taking a print screen as one would do normally using snipping tool or MS paint. The crop function you suggested allowed me to scale and remove unnecessary info from the rest of the view such as taskbar etc. I only NEED to capture say a 1000x1000 view (because the info I need is encapsulated in that part of desktop view). I will manually alter the macro to suit the view needed. More pressing is the lack of screen refresh, not the view itself. Thanks. – loco May 17 '17 at 15:30
  • can you answer the question? doesn't matter if the macro doesn't interact with other apps. I want to know which app(s) you need to capture in the screenshot... sure, a 1000x1000 square might encapsulate these, but i think it might be stupid-easy to capture a specific app, and that's why I keep pressing you to answer this question... please assume I know what I'm doing and I am asking questions with purpose :) – David Zemens May 17 '17 at 15:42
  • Sure, the application is software called Olympic as well as some word document, other excel files and PDF's. Basically, a variation of applications. I want to be able to extend functionality to future apps and other items in view that I may not have necessarily covered in this list. – loco May 17 '17 at 15:45
  • ok sure so you're trying to get them all at once so a screenshot of the monitor is pronably better than trying to cap each app separately. I have an idea but not at computer to test it yet, it will involve using the other method, not SendKeys. otherwise I don't know why the clipboard isn't copying correctly, I tried 2 or 3 things you force it but none working correctly... – David Zemens May 17 '17 at 15:49
  • Sorry, I may have miscommunicated. The view generated by the button will only be a specific segment from within a single chosen app. I will not have multiple applications in a view at one time. The views im capturing are evidence files for a report hence the snapshots are specific areas. I will be expanding my excel to include a button for each type of screenshot located in different areas of an excel file. i.e scaling up once I have a working screenshot button macro that properly refreshes the screen. It is nearly there, it just needs to refresh to the current view with each click. Thanks. – loco May 17 '17 at 15:53
  • Thank you for updating the thread David. I will test this solution tomorrow and let you know. – loco May 17 '17 at 22:22
  • This works. However, I attempted to use shp.TopLeftCell to position the screen capture and it failed. I'd like the top left of the *screenshot* to be on whichever cell I click. Something like shp.ActiveCell.Paste or ActiveCell.Paste but with several attempts of syntax this has failed. I can begin this as a new thread if required since you technically answered my original question. – loco May 18 '17 at 09:04
  • Example: Dim MyCell As Range. Set MyCell = ActiveCell. This works but the active cell is a distance of h(?) away from the top left of the image because of the crop. So it pastes the image h distance away from whichever active cell is selected. – loco May 18 '17 at 09:18
  • crop the image BEFORE you set its position on the sheet. The shadow doesn't get "locked" to that location, so if you place it there and then crop it, it will naturally no longer align with that position... `Set shp.TopLeftCell = AcriveCell.` – David Zemens May 18 '17 at 10:55
  • Set shp.TopLeftCell = ActiveCell brings up invalid use of property when compiling (at any location within my code). I removed MyCell as a variable and tested that statement alone. – loco May 18 '17 at 14:49
  • Get rid of `set` keyword in that case. I don't mean to be rude, but so much of this is stuff that you need to learn how to troubleshoot on your own and I simply can't keep answering every tiny one-off question you have pertaining to this implementation. Google is your friend. Find the object model reference, study it, start there, and look for similar Q's on this forum if you get stuck. – David Zemens May 18 '17 at 15:01
7

You can try this code in a standard Module in Excel 32 Bit.

  • Screenshots can be captured immediately by calling Sub prcSave_Picture_Screen and it will capture your whole screen and save to the same path as your workbook (You can change the path and file name if you want)
  • Screenshots of an active window can also be captured after calling Sub prcSave_Picture_Active_Window 3 seconds (which is adjustable)

Source: ms-office-forum.de

Option Explicit

Private Declare Sub Sleep Lib "kernel32.dll" ( _
    ByVal dwMilliseconds As Long)
Private Declare Function OleCreatePictureIndirect Lib "olepro32.dll" ( _
    ByRef PicDesc As PicBmp, _
    ByRef RefIID As GUID, _
    ByVal fPictureOwnsHandle As Long, _
    ByRef IPic As IPicture) As Long
Private Declare Function CreateCompatibleDC Lib "gdi32.dll" ( _
    ByVal hdc As Long) As Long
Private Declare Function CreateCompatibleBitmap Lib "gdi32.dll" ( _
    ByVal hdc As Long, _
    ByVal nWidth As Long, _
    ByVal nHeight As Long) As Long
Private Declare Function SelectObject Lib "gdi32.dll" ( _
    ByVal hdc As Long, _
    ByVal hObject As Long) As Long
Private Declare Function GetDeviceCaps Lib "gdi32.dll" ( _
    ByVal hdc As Long, _
    ByVal iCapabilitiy As Long) As Long
Private Declare Function GetSystemPaletteEntries Lib "gdi32.dll" ( _
    ByVal hdc As Long, _
    ByVal wStartIndex As Long, _
    ByVal wNumEntries As Long, _
    ByRef lpPaletteEntries As PALETTEENTRY) As Long
Private Declare Function CreatePalette Lib "gdi32.dll" ( _
    ByRef lpLogPalette As LOGPALETTE) As Long
Private Declare Function SelectPalette Lib "gdi32.dll" ( _
    ByVal hdc As Long, _
    ByVal hPalette As Long, _
    ByVal bForceBackground As Long) As Long
Private Declare Function RealizePalette Lib "gdi32.dll" ( _
    ByVal hdc As Long) As Long
Private Declare Function BitBlt Lib "gdi32.dll" ( _
    ByVal hDestDC As Long, _
    ByVal x As Long, _
    ByVal y As Long, _
    ByVal nWidth As Long, _
    ByVal nHeight As Long, _
    ByVal hSrcDC As Long, _
    ByVal xSrc As Long, _
    ByVal ySrc As Long, _
    ByVal dwRop As Long) As Long
Private Declare Function DeleteDC Lib "gdi32.dll" ( _
    ByVal hdc As Long) As Long
Private Declare Function GetDC Lib "user32.dll" ( _
    ByVal hWnd As Long) As Long
Private Declare Function GetWindowRect Lib "user32.dll" ( _
    ByVal hWnd As Long, _
    ByRef lpRect As RECT) As Long
Private Declare Function GetSystemMetrics Lib "user32.dll" ( _
    ByVal nIndex As Long) As Long
Private Declare Function GetForegroundWindow Lib "user32.dll" () As Long

Private Const SM_CXSCREEN = 0&
Private Const SM_CYSCREEN = 1&
Private Const RC_PALETTE As Long = &H100
Private Const SIZEPALETTE As Long = 104
Private Const RASTERCAPS As Long = 38

Private Type RECT
    Left As Long
    Top As Long
    Right As Long
    Bottom As Long
End Type

Private Type PALETTEENTRY
    peRed As Byte
    peGreen As Byte
    peBlue As Byte
    peFlags As Byte
End Type

Private Type LOGPALETTE
    palVersion As Integer
    palNumEntries As Integer
    palPalEntry(255) As PALETTEENTRY
End Type

Private Type GUID
    Data1 As Long
    Data2 As Integer
    Data3 As Integer
    Data4(7) As Byte
End Type

Private Type PicBmp
    Size As Long
    Type As Long
    hBmp As Long
    hPal As Long
    Reserved As Long
End Type

Public Sub prcSave_Picture_Screen() 'ganzer bildschirm
    stdole.SavePicture hDCToPicture(GetDC(0&), 0&, 0&, _
        GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN)), _
        ThisWorkbook.Path & "\Screenshot.bmp" 'anpassen !!!
End Sub

Public Sub prcSave_Picture_Active_Window() 'aktives Fenster
    Dim hWnd As Long
    Dim udtRect As RECT
    Sleep 3000 '3 sekunden pause um ein anderes Fenster zu aktivieren
    hWnd = GetForegroundWindow
    GetWindowRect hWnd, udtRect
    stdole.SavePicture hDCToPicture(GetDC(0&), udtRect.Left, udtRect.Top, _
        udtRect.Right - udtRect.Left, udtRect.Bottom - udtRect.Top), _
        ThisWorkbook.Path & "\Screenshot.bmp" 'anpassen !!!
End Sub

Private Function CreateBitmapPicture(ByVal hBmp As Long, ByVal hPal As Long) As Object
    Dim Pic As PicBmp, IPic As IPicture, IID_IDispatch As GUID
    With IID_IDispatch
        .Data1 = &H20400
        .Data4(0) = &HC0
        .Data4(7) = &H46
    End With
    With Pic
        .Size = Len(Pic)
        .Type = 1
        .hBmp = hBmp
        .hPal = hPal
    End With
    Call OleCreatePictureIndirect(Pic, IID_IDispatch, 1, IPic)
    Set CreateBitmapPicture = IPic
End Function

Private Function hDCToPicture(ByVal hDCSrc As Long, ByVal LeftSrc As Long, _
    ByVal TopSrc As Long, ByVal WidthSrc As Long, ByVal HeightSrc As Long) As Object
    Dim hDCMemory As Long, hBmp As Long, hBmpPrev As Long
    Dim hPal As Long, hPalPrev As Long, RasterCapsScrn As Long, HasPaletteScrn As Long
    Dim PaletteSizeScrn As Long, LogPal As LOGPALETTE
    hDCMemory = CreateCompatibleDC(hDCSrc)
    hBmp = CreateCompatibleBitmap(hDCSrc, WidthSrc, HeightSrc)
    hBmpPrev = SelectObject(hDCMemory, hBmp)
    RasterCapsScrn = GetDeviceCaps(hDCSrc, RASTERCAPS)
    HasPaletteScrn = RasterCapsScrn And RC_PALETTE
    PaletteSizeScrn = GetDeviceCaps(hDCSrc, SIZEPALETTE)
    If HasPaletteScrn And (PaletteSizeScrn = 256) Then
        LogPal.palVersion = &H300
        LogPal.palNumEntries = 256
        Call GetSystemPaletteEntries(hDCSrc, 0, 256, LogPal.palPalEntry(0))
        hPal = CreatePalette(LogPal)
        hPalPrev = SelectPalette(hDCMemory, hPal, 0)
        Call RealizePalette(hDCMemory)
    End If
    Call BitBlt(hDCMemory, 0, 0, WidthSrc, HeightSrc, hDCSrc, LeftSrc, TopSrc, 13369376)
    hBmp = SelectObject(hDCMemory, hBmpPrev)
    If HasPaletteScrn And (PaletteSizeScrn = 256) Then
        hPal = SelectPalette(hDCMemory, hPalPrev, 0)
    End If
    Call DeleteDC(hDCMemory)
    Set hDCToPicture = CreateBitmapPicture(hBmp, hPal)
End Function
Đức Thanh Nguyễn
  • 9,127
  • 3
  • 21
  • 27
  • 1
    Works fine in x64 Excel too, as long as you change all the Private Declares to Private Declare PtrSafe, and change the reference to olepro32.dll to be oleaut32.dll. – Chris Rae Oct 05 '22 at 01:24
0
Sub SavePicToFile(namefile)
 Selection.CopyPicture xlScreen, xlBitmap
 Application.DisplayAlerts = False
 Set tmp = Charts.Add
 On Error Resume Next
 With tmp
    .SeriesCollection(1).Delete
    .Width = Selection.Width
    .Height = Selection.Height
    .Paste
    .Export filename:=namefile, Filtername:="jpeg"
    .Delete
 End With
End Sub
foto = Application.ActiveWorkbook.Path & "\Foto" & ".jpeg"
ActiveWorkbook.Sheets(1).Range("A1:Z30").Select
SavePicToFile (foto)