qt icon indicating copy to clipboard operation
qt copied to clipboard

Taking screen shot of another open (and focused) window

Open amlwwalker opened this issue 4 years ago • 7 comments

I've been trying to find a sweet way to take a screen shot of another window that is open and has the current focus. I see there is this Qt/qml python app http://www.ifnamemain.com/posts/2014/Jul/11/screen_capture_1/ Which would for one thing be an awesome port over to Go, but also demonstrates that screen shotting other windows could be possible. I also came across this library, robotgo which is really close to being a work around - it can get the currently focused window, it can get the coordinates of the window and it can take screen shots at coordinates. Unfortunately there is a bug on Mac where it doesn't get valid coordinates and crashes. I've asked a question here but no progress. https://github.com/go-vgo/robotgo/issues/230 #

The above two however suggest that this may all be something that could be done in go and with Qt (so it works cross platform).

For transparency im hoping to set a global shortcut that takes a screen shot of the currently focused window. Cheekily asking for some help here... Thanks!!

amlwwalker avatar Oct 24 '19 07:10 amlwwalker

Hey

I looked into this, but to no avail. I will look into this again tomorrow though, once I have some more time.

therecipe avatar Oct 25 '19 04:10 therecipe

awesome. Thanks so much. Qt is blowing my mind with its api into the OS capabilities FYI realise this isn't Go/Qt, but its C++ and is successfully doing screen recording -> gif. It's not quite what I'm after as I want a screenshot of the current focused window automatically, but neat how it works! https://www.cockos.com/licecap/

amlwwalker avatar Oct 25 '19 08:10 amlwwalker

Sorry for the delay, but I looked into this more deeply and it seems like it's not possible to get the bounds of a 3rd-party window under macOS. (probably due to security and/or privacy concerns)

Take a look here, you can get the bounds and then "steal" the window content if the application/process owns the window. But the same code is not working with 3rd-party windows, because for those AXUIElementCreateApplication only returns NULL.

You can test it by replacing someProcessName with a valid process name, otherwise the bounds of the test window are printed from C and then later, once Qt "stole" the window they are printed from Go as well.

main.go

package main

/*
#cgo LDFLAGS: -framework AppKit
void* getWinId(int);
*/
import "C"

import (
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/go-vgo/robotgo"

	"github.com/therecipe/qt/core"
	"github.com/therecipe/qt/gui"
	"github.com/therecipe/qt/widgets"
)

type mainHelper struct {
	core.QObject
	_ func(f func()) `slot:"runOnMainHelper,auto"`
}

func (*mainHelper) runOnMainHelper(f func()) { f() }

func main() {

	app := widgets.NewQApplication(len(os.Args), os.Args)
	mainHelper := NewMainHelper(nil)

	testWindow := widgets.NewQMainWindow(nil, 0)
	testWindow.SetCentralWidget(widgets.NewQPushButton2("Some PushButton", nil))
	testWindow.SetWindowTitle("TestWindow")
	testWindow.Show()

	someProcessName := "someProcessName"

	go func() {
		time.Sleep(3 * time.Second)

		var pid int
		process, _ := robotgo.Process()
		for _, n := range []string{someProcessName, filepath.Base(os.Args[0])} {
			for _, p := range process {
				if strings.Contains(p.Name, n) {
					robotgo.SetActive(robotgo.GetHandPid(p.Pid))
					pid = int(p.Pid)
					break
				}
			}
			if pid != 0 {
				break
			}
		}

		wId := uintptr(C.getWinId(C.int(pid)))
		println("wId from Go:", wId)
		if wId == 0 {
			return
		}

		mainHelper.RunOnMainHelper(func() {
			mainWindow := widgets.NewQMainWindow(nil, 0)
			mainWindow.SetWindowTitle("MainWindow")
			mainWindow.SetCentralWidget(widgets.QWidget_CreateWindowContainer(gui.QWindow_FromWinId(wId), nil, 0))
			mainWindow.Show()
			println("bounds from Go:", mainWindow.X(), mainWindow.Y(), mainWindow.X()+mainWindow.FrameGeometry().Width(), mainWindow.Y()+mainWindow.FrameGeometry().Height()) //https://doc.qt.io/qt-5/application-windows.html#window-geometry

			app.PrimaryScreen().GrabWindow(wId, mainWindow.X(), mainWindow.Y(), mainWindow.FrameGeometry().Width(), mainWindow.FrameGeometry().Height()).Save("someImage.png", "", -1)
		})
	}()

	widgets.QApplication_Exec()
}

main.m


#import <AppKit/AppKit.h>

AXError _AXUIElementGetWindow(AXUIElementRef, CGWindowID* out);

void* getWinId(int pid)
{
  NSWindow * hwnd;

  NSArray *windows;
  AXUIElementCopyAttributeValues(AXUIElementCreateApplication(pid), kAXWindowsAttribute, 0, 100, (CFArrayRef *) &windows);

  for (id child in windows) {
    CGWindowID windowId;
    _AXUIElementGetWindow((AXUIElementRef) child, &windowId);
    
    hwnd  = [NSApp windowWithWindowNumber: windowId];
    NSLog(@"hwnd from C: %lu", (uintptr_t)hwnd);

    NSRect rect = [hwnd frame];
    int scrh = CGDisplayPixelsHigh(CGMainDisplayID());
    NSLog(@"bounds from C: %i %i %i %i", (int)rect.origin.x, scrh - (int)rect.origin.y - (int)rect.size.height, (int)rect.origin.x + (int)rect.size.width, scrh - (int)rect.origin.y);
    break;
  }

  return [hwnd contentView];
}

I also looked into a way to work around this, but the only solution I came up is rather hacky:

package main

import (
	"os"

	"github.com/go-vgo/robotgo"

	"github.com/therecipe/qt/core"
	"github.com/therecipe/qt/gui"
	"github.com/therecipe/qt/widgets"

	"github.com/therecipe/qt/internal/examples/3rdparty/uglobalhotkey/UGlobalHotkey"
)

func main() {
	app := widgets.NewQApplication(len(os.Args), os.Args)

	testWindow := widgets.NewQMainWindow(nil, 0)
	testWindow.SetWindowTitle("TestWindow")
	testWindow.SetStyleSheet("QMainWindow {background: '#fafbfc';}")
	testWindow.SetFixedSize(app.PrimaryScreen().Size())
	testWindow.Move2(0, 0)

	hk := UGlobalHotkey.NewUGlobalHotkeys(nil)
	hk.RegisterHotkey("Ctrl+Shift+A", 1)
	hk.ConnectActivated(func(id uint) {
		println("Activated:", id)

		pid := robotgo.GetPID()

		testWindow.Show()
		testWindow.Raise()
		app.ProcessEvents(0)

		robotgo.ActivePID(pid)
		robotgo.ActivePID(pid)

		pix := app.PrimaryScreen().GrabWindow(0, testWindow.X(), testWindow.Y(), testWindow.FrameGeometry().Width(), testWindow.FrameGeometry().Height())
		testWindow.Hide()
		app.ProcessEvents(0)

		pix.Save("someImageOriginal.png", "", -1)

		//

		mask := pix.CreateMaskFromColor(gui.NewQColor8("#fafbfc"), core.Qt__MaskOutColor)
		pix.SetDevicePixelRatio(1)

		p := gui.NewQPainter2(pix)
		p.SetPen2(gui.NewQColor3(0, 255, 0, 255))
		p.DrawPixmap2(pix.Rect(), mask, mask.Rect())
		p.End()

		pix.Save("someImageMasked.png", "", 0)
	})

	widgets.QApplication_Exec()
}

therecipe avatar Nov 01 '19 23:11 therecipe

Ok thanks, I will test later to understand exactly what you mean, I think you say that the first does not work for any third party window, and the second does but is a hack. Tbh of I can get an image of a third party window, that will be a good proof of concept for me at this stage, and with time can perhaps be improved of needs be. Thanks again! Will test in an hour or so

EDIT: Attempting to run now, I have upgraded to go version 1.13

go version go1.13.3 darwin/amd64

And when I now compile anything with therecipe/qt. I receive an error about no stdlib.h -

/local/go/bin:/Users/alex/go/bin:/Users/alex/Library/Android/sdk/platform-toolsATH=Qt/5.8/clang_64/libATH=Qt/5.8/clang_64/bin" error="exit status 2" name="build for darwin on darwin"
github.com/therecipe/qt/core
# github.com/therecipe/qt/core
clang: warning: no such sysroot directory: '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk' [-Wmissing-sysroot]
../../../../therecipe/qt/core/core.go:6:10: fatal error: 'stdlib.h' file not found
#include <stdlib.h>
         ^~~~~~~~~~
1 error generated.

Any ideas what the fix is? 'Normal' (i.e not therecipe/qt) compile no problem.

A

EDIT 2: reinstalling the binding fixed this. Do you have to have export GO111MODULE=off; to run this with Go 1.13?

amlwwalker avatar Nov 04 '19 09:11 amlwwalker

Ok... Thanks for your examples, they resulted in me digging harder for a solution. I guess that if the user is on windows or Linux then the out of the box screenshot technique from robotgo that bugs out on mac will work, so I can just use that. I did however solve the problem on mac, but I think if you think your technique is a hack, you will laugh at this one...

package main

import (
	"fmt"
	"os"
	"strconv"
	"strings"

	"github.com/everdev/mack"
	"github.com/go-vgo/robotgo"

	"github.com/therecipe/qt/widgets"

	"github.com/amlwwalker/experiments/screenshots/example2/UGlobalHotkey"
	//"github.com/therecipe/qt/internal/examples/3rdparty/uglobalhotkey/UGlobalHotkey"
)

func main() {
	app := widgets.NewQApplication(len(os.Args), os.Args)
	hk := UGlobalHotkey.NewUGlobalHotkeys(nil)
	hk.RegisterHotkey("Ctrl+Shift+A", 1)
	hk.ConnectActivated(func(id uint) {
		println("Activated:", id)
		activeMeta := robotgo.GetActive()
		fmt.Printf("%+v\r\n", activeMeta)
		pidInt32 := robotgo.GetPID()
		pidString := strconv.Itoa(int(pidInt32))
		response, err := mack.Tell("System Events", "set _P to a reference to (processes whose unix id is "+pidString+")", "set _W to a reference to windows of _P", "[_P's name, _W's size, _W's position]")
		if err != nil {
			fmt.Println("couldnt get dimensions and position")
		} else {
			fmt.Println(response)
			takeScreenShot(response)
		}
		app.ProcessEvents(0)
	})

	widgets.QApplication_Exec()
}

func takeScreenShot(response string) error {
	type window struct {
		name string
		w    int
		h    int
		x    int
		y    int
	}
	splt := strings.Split(response, ", ")
	var w window
	w.name = splt[0]

	var err error
	if w.w, err = strconv.Atoi(splt[1]); err != nil {
		return err
	}
	if w.h, err = strconv.Atoi(splt[2]); err != nil {
		return err
	}
	if w.x, err = strconv.Atoi(splt[3]); err != nil {
		return err
	}
	if w.y, err = strconv.Atoi(splt[4]); err != nil {
		return err
	}
	fmt.Printf("%+v\r\n", w)
	robotgo.SaveCapture("saveCapture.png", w.x, w.y, w.w, w.h)
	return nil
}

AppleScript!!! 😂

It works a charm! would probably need to put some more checks in place for instance that the ID is valid or something, (any thoughts??) but it does indeed work!

My applescript skills are absolutely terrible but I wonder if you can get the drop shadow on the screenshots that you get when you use the built in screenshot tool, or can remove the border/menu bar etc.

side note: how do you specify the output file name (the app name) - with go build you can specify an -o flag for the output, how can I do this with qtdeploy?

btw, have you ported this example by any chance? Searching the examples isn't the easiest :) https://doc.qt.io/qt-5/qtwidgets-richtext-syntaxhighlighter-example.html

amlwwalker avatar Nov 04 '19 20:11 amlwwalker

reinstalling the binding fixed this. Do you have to have export GO111MODULE=off; to run this with Go 1.13?

I think this was because you probably did update Xcode as well? Also, yeah if you want to use the binding in "GOPATH mode" then you will need to disable the module support. Go 1.13 silently changed the behavior from opt-in to opt-out for go modules ...

I did however solve the problem on mac, but I think if you think your technique is a hack, you will laugh at this one...

:D yes, that's nice. I didn't know you could do that with AppleScript!

(any thoughts??) but it does indeed work!

I'm not sure, it didn't work for me the first try. But did it did work the second and all following times. Maybe check if you can detect the windows "traffic lights" or something? But I'm not really sure, since you can basically create a borderless windows and override these things as well. (but it's maybe better than nothing though?)

My applescript skills are absolutely terrible but I wonder if you can get the drop shadow on the screenshots that you get when you use the built in screenshot tool, or can remove the border/menu bar etc.

I'm not sure about that either, I think getting a "taintless" drop shadow is quite hard. Maybe combine this with my hack, and move a white window underneath the active window before screenshooting? That way the drop shadows is only tainted with the white background and can be restored somehow? Or what about using apple script to automate the build in screen shot tool?

how can I do this with qtdeploy?

Ah shit, sorry that's not possible atm, I wrote this down to look into but it got buried already. (I also still have the examples in mind we talked about a while back, about how []*core.QVariant and []*core.QObject could be used as models in Qml, but haven't had time to work on them either)

btw, have you ported this example by any chance? Searching the examples isn't the easiest :)

I didn't, but I know someone who did: https://github.com/5k3105/GolangHighlighter And here I used a slight edited version to support Go as well as JS: https://github.com/therecipe/widgets_playground/blob/master/highlighter.go

therecipe avatar Nov 08 '19 01:11 therecipe

  response, err := mack.Tell("System Events", "set _P to a reference to (processes whose unix id is "+pidString+")", "set _W to a reference to windows of _P", "[_P's name, _W's size, _W's position]")

Huge legend! Hacky, but works perfectly. Thank you

archibold9 avatar Dec 17 '20 22:12 archibold9