Search code examples
pythonpython-3.xuser-interfacekivykivy-language

Almost working way to grab a borderless window in Kivy


I am looking to replace the windows title bar for a borderless app, I found some solutions on the internet that didn't quite work for me so I tried to do it myself.

Although the grabbing the screen and moving part works, once you release the click, the window continues to follow the cursor until eventually the program stops responding and the task is terminated.

This is an example of code that I prepared with some indications on how it works:

from kivy.app import App
from win32api import GetSystemMetrics
from kivy.lang.builder import Builder
from kivy.core.window import Window
from kivy.uix.widget import Widget
import pyautogui
import win32api
import re


Window.size=(600,300)
Window.borderless=True
#The following causes the window to open in the middle of the screen:
Window.top=((GetSystemMetrics(1)/2)-150)
Window.left=((GetSystemMetrics(0)/2)-300) 
#####################################################################

Builder.load_string("""
<Grab>
    GridLayout:
        size:root.width,root.height
        cols:2
        Label:
            id:label
            text:'A label'
        Button:
            id:button
            text:'The button that changes the window position'
            on_press: root.grab_window()
""")

class Grab(Widget):
    def grab_window(self):
        #The following saves the mouse position relative to the window:
        Static_Mouse_pos=re.findall('\d+',str(pyautogui.position()))
        Mouse_y=int(Static_Mouse_pos[1])-Window.top
        Mouse_x=int(Static_Mouse_pos[0])-Window.left
        ###############################################################
        #The following is what causes the window to follow the mouse position:
        while win32api.GetKeyState(0x01)<0: #In theory this should cause the loop to start as soon as it is clicked, I ruled out that it would start and end when the button was pressed and stopped being pressed because as soon as the screen starts to move, it stops being pressed.
            Relative_Mouse_pos=re.findall('\d+',str(pyautogui.position()))
            Window.left=(int(Relative_Mouse_pos[0])-Mouse_x)
            Window.top=(int(Relative_Mouse_pos[1])-Mouse_y)
            print(f'Mouse position: ({Mouse_x},{Mouse_y})') #To let you know the mouse position (Not necessary)
            print(f'Window position: ({Window.top},{Window.left})') #To let you know the position of the window (Not necessary)
            if win32api.GetKeyState(0x01)==0: #This is supposed to stop everything (Detects when you stop holding the click)
                break
        ######################################################################
class app(App):
    def build(self):
        return Grab()
if __name__=='__main__':
    app().run()

Is there a way to make it work fine? Or another way to grab a borderless window that might be effective?

I'm new to programming, so I apologize in advance for any nonsense you may read in my code.

EDIT: For some reason win32api.GetKeyState(0x01) is not updated once the click is done and the loop is started, nor does it help to make a variable take its value.


Solution

  • I've finally come up with a solution but this may not be the best one.

    ( Some places where I made changes are marked with comment [Modified] )

    from kivy.app import App
    from win32api import GetSystemMetrics  # for getting screen size
    from kivy.lang.builder import Builder
    from kivy.core.window import Window
    from kivy.uix.widget import Widget
    import pyautogui
    # import win32api
    # import re
    
    # set window size
    # Window.size=(600,300)
    
    # make the window borderless
    Window.borderless = True
    
    # The following causes the window to open in the middle of the screen :
    Window.left = ((GetSystemMetrics(0) / 2) - Window.size[0] / 2)  # [Modified] for better flexibility
    Window.top = ((GetSystemMetrics(1) / 2) - Window.size[1] / 2)  # [Modified] for better flexibility
    #####################################################################
    
    Builder.load_string("""
    <Grab>
        GridLayout:
            size: root.width, root.height
            rows: 2  # [modified]
            
            Button:
                id: button
                text: "The button that changes the window position"
                size_hint_y: 0.2
                
            Label:
                id: label
                text: "A label"
    """)
    
    
    class Grab(Widget):
        
        # I'm sorry I just abandoned this lol
        """
        def grab_window(self):
            #The following saves the mouse position relative to the window:
            Static_Mouse_pos=re.findall('\d+',str(pyautogui.position()))
            Mouse_y=int(Static_Mouse_pos[1])-Window.top
            Mouse_x=int(Static_Mouse_pos[0])-Window.left
            ###############################################################
            #The following is what causes the window to follow the mouse position:
            while win32api.GetKeyState(0x01)<0: #In theory this should cause the loop to start as soon as it is clicked, I ruled out that it would start and end when the button was pressed and stopped being pressed because as soon as the screen starts to move, it stops being pressed.
                Relative_Mouse_pos=re.findall('\d+',str(pyautogui.position()))
                Window.left=(int(Relative_Mouse_pos[0])-Mouse_x)
                Window.top=(int(Relative_Mouse_pos[1])-Mouse_y)
                print(f'Mouse position: ({Mouse_x},{Mouse_y})') #To let you know the mouse position (Not necessary)
                print(f'Window position: ({Window.top},{Window.left})') #To let you know the position of the window (Not necessary)
                if win32api.GetKeyState(0x01)==0: #This is supposed to stop everything (Detects when you stop holding the click)
                    break
            ######################################################################
        """
        
        def on_touch_move(self, touch):
            
            if self.ids.button.state == "down":  # down | normal
                # button is pressed
        
                # mouse pos relative to screen , list of int
                # top left (0, 0) ; bottom right (max,X, maxY)
                mouse_pos = [pyautogui.position()[0], pyautogui.position()[1]]
            
                # mouse pos relative to the window
                # ( normal rectangular coordinate sys. )
                mouse_x = touch.pos[0]
                mouse_y = Window.size[1] - touch.pos[1]  # since the coordinate sys. are different , just to converse it into the same
                
                # give up using touch.dx and touch.dy , too lag lol
                
                Window.left = mouse_pos[0] - mouse_x
                Window.top = mouse_pos[1] - mouse_y
                
    
    class MyApp(App):  # [Modified] good practice using capital letter for class name
        
        def build(self):
            return Grab()
    
    
    if __name__ == "__main__":
        MyApp().run()
    
    

    I just gave up using button on_press or on_touch_down as suggested in the comment since it requires manual update for the mouse position.

    Instead , I try using the Kivy built-in function ( ? ) on_touch_move. It is fired when a mouse motion is detected inside the windows by the application itself. ( much more convenient compared with manual checking lol )

    The concepts of window positioning are similar to yours , which is mouse pos relative to screen - mouse pos relative to app window. But the coordinate system used by Pyautogui and Kivy 's window are different , therefore I did some conversion for this as seen in the code above.

    But I'm not sure whether the unit used by Pyautogui and Kivy for mouse positioning is the same or not ( ? ), so it would not be as smooth as expected / ideal case when drag-and-dropping the window via the button. Also the time delay for updating when on_touch_move of the kivy app. That's the reason why I think it may be no the best answer for your question.

    Any other solutions / suggestions / improvements etc. are welcome : )


    Simplified Code For Copy-Paste :

    Edit : Added close / minimize window button ( at top-left corner )

    #
    # Windows Application
    # Borderless window with button press to move window
    #
    
    import kivy
    from kivy.app import App
    from kivy.core.window import Window
    from kivy.uix.widget import Widget
    from kivy.clock import Clock
    
    from win32api import GetSystemMetrics  # for getting screen size
    import pyautogui  # for getting mouse pos
    
    # set window size
    # Window.size = (600,300)
    
    # make the window borderless
    Window.borderless = True
    
    # set init window pos : center
    # screen size / 2 - app window size / 2
    Window.left = (GetSystemMetrics(0) / 2) - (Window.size[0] / 2)
    Window.top = (GetSystemMetrics(1) / 2) - (Window.size[1] / 2)
    
    kivy.lang.builder.Builder.load_string("""
    <GrabScreen>
        Button:
            id: close_window_button
            text: "[b] X [b]"
            font_size: 25
            markup: True
            background_color: 1, 1, 1, 0
            size: self.texture_size[0] + 10, self.texture_size[1] + 10
            border: 25, 25, 25, 25
            
            on_release:
                root.close_window()
            
        Button:
            id: minimize_window_button
            text: "[b] - [b]"
            font_size: 25
            markup: True
            background_color: 1, 1, 1, 0
            size: self.texture_size[0] + 10, self.texture_size[1] + 10
            border: 25, 25, 25, 25
            
            on_release:
                root.minimize_window()
            
        Button:
            id: move_window_button
            text: "[b]. . .[/b]"
            font_size: 25
            markup: True
            background_color: 1, 1, 1, 0
            width: root.width / 3
            height: self.texture_size[1] * 1.5
            border: 25, 25, 25, 25
    
        Label:
            id: this_label
            text: "Hello World !"
            font_size: 25
            size: self.texture_size
    """)
    
    
    class GrabScreen(Widget):
        
        def close_window(self):
            App.get_running_app().stop()
            
        
        # def on_window_minimize(self, *args, **kwargs):
        #     print(args)
        
        
        def minimize_window(self):
            Window.minimize()
            # Window.bind(on_minimize = self.on_window_minimize)
        
        
        def maximize_window(self):
            Window.size = [GetSystemMetrics(0), GetSystemMetrics(1)]
            Window.left = 0
            Window.top = 0
            
            
        def update(self, dt):
            # button for closing window
            self.ids.close_window_button.top = self.top
            
            # button for minimizing window
            self.ids.minimize_window_button.x = self.ids.close_window_button.right
            self.ids.minimize_window_button.top = self.top
            
            # button for moving window
            self.ids.move_window_button.center_x = self.center_x
            self.ids.move_window_button.top = self.top
            
            # label
            self.ids.this_label.center = self.center
            
            
        def on_touch_move(self, touch):
            # when touching app screen and moving
            
            if self.ids.move_window_button.state == "down":  # down | normal
                # (button move_window_button is pressed) and (mouse is moving)
    
                # mouse pos relative to screen , list of int
                # top left (0, 0) ; bottom right (maxX, maxY)
                mouse_pos = [pyautogui.position()[0], pyautogui.position()[1]]  # pixel / inch
    
                # mouse pos relative to the window
                # ( normal rectangular coordinate sys. )
                # since the coordinate sys. are different , just to converse it to fit that of pyautogui
                # Note :
                #   1 dpi = 0.393701 pixel/cm
                #   1 inch = 2.54 cm
                """
                n dot/inch = n * 0.393701 pixel/cm
                1 pixel/cm = 2.54 pixel/inch
                n dot/inch = n * 0.393701 * 2.54 pixel/inch
                """
                mouse_x = touch.x  # dpi
                mouse_y = self.height - touch.y  # dpi
    
                # update app window pos
                Window.left = mouse_pos[0] - mouse_x
                Window.top = mouse_pos[1] - mouse_y
                
                # max / min window
                if mouse_pos[1] <= 1:
                    self.maximize_window()
                
                elif Window.size[0] >= GetSystemMetrics(0) and Window.size[1] >= GetSystemMetrics(1):
                    Window.size = [Window.size[0] / 2, Window.size[1] * 0.7]
    
    
    class MyApp(App):
        grabScreen = GrabScreen()
        
        
        def build(self):
            # schedule update
            Clock.schedule_interval(self.grabScreen.update, 0.1)
            
            return self.grabScreen
    
    
    if __name__ == "__main__":
        MyApp().run()
    

    Reference

    Kivy Motion Event