Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get rid of the 6px white frame caused by WS_THICKFRAME #13

Closed
littlewhitecloud opened this issue Jan 3, 2023 · 37 comments
Closed

Get rid of the 6px white frame caused by WS_THICKFRAME #13

littlewhitecloud opened this issue Jan 3, 2023 · 37 comments
Labels
✨ enhancement New feature or request 📗 help wanted Extra attention is needed ❌ invalid This doesn't seem right

Comments

@littlewhitecloud
Copy link
Owner

image

@littlewhitecloud littlewhitecloud added 🐞 bug Something isn't working ✨ enhancement New feature or request 📗 help wanted Extra attention is needed ❌ invalid This doesn't seem right 💬 question Further information is requested 🕰 future 🧰 "Windows" API Error It just windows's api's gui's error? labels Jan 3, 2023
@littlewhitecloud littlewhitecloud added this to the Windows Gui error milestone Jan 5, 2023
@HuyHung1408
Copy link

HuyHung1408 commented Apr 30, 2023

image

Having this in Windows 11 too, else it looks cool tbh 😊.

@littlewhitecloud
Copy link
Owner Author

littlewhitecloud commented May 1, 2023

image

Having this in Windows 11 too, else it looks cool tbh 😊.

Thank you very much for testing this project on windows11!
I used some tricks on the window in the lastest commit, I think we won’t see the white frame now…(just as the same color as titlebar)

@HuyHung1408 Can you use my latest commit to test on the windows11 again? I want the latest screenshot… Thank you!

@littlewhitecloud littlewhitecloud pinned this issue May 1, 2023
@littlewhitecloud littlewhitecloud unpinned this issue May 1, 2023
@littlewhitecloud littlewhitecloud pinned this issue May 1, 2023
@HuyHung1408
Copy link

image

image

@littlewhitecloud here you go, it's actually using dark mica effect in the latest code, instead of the white mica effect, which is better now. I think this happens because Microsoft forces all windows' title bar to have mica effect since Windows 11 22H2. I don't think this will happen in other Windows version.

@littlewhitecloud
Copy link
Owner Author

@HuyHung1408 Thank you very much!

@littlewhitecloud
Copy link
Owner Author

@Akascape, can you fix it...? If you can I will be very grateful.

@littlewhitecloud littlewhitecloud removed 💬 question Further information is requested 🕰 future 🧰 "Windows" API Error It just windows's api's gui's error? 🐞 bug Something isn't working labels Jun 23, 2023
@Akascape
Copy link

Akascape commented Jun 23, 2023

@littlewhitecloud Ok, I have found the fix, but let me tell you all the details. What we have to do is match the titlebar color with the frame.
In windows 11, the title bar color can be changed using this method:

color = 0x00BBGGRR # this is the color order bgr

ctypes.windll.dwmapi.DwmSetWindowAttribute(windll.user32.GetParent(window.winfo_id()), 35, byref(c_int(color)), sizeof(c_int(2)))

One problem is that color string cannot be passed directly. The color API used here is this: https://learn.microsoft.com/en-us/windows/win32/gdi/colorref ( blue and red part is interchanged [rgb and bgr order])
But conversion is possible between hex string and that api order. See this: https://github.com/Akascape/py-window-styles

Basically, you have to change this line:

windll.dwmapi.DwmSetWindowAttribute(
windll.user32.GetParent(self.winfo_id()), 20, byref(c_int(2)), sizeof(c_int(2))
)

example :

I used this color ff0000 (bgr) as example.
Screenshot

@Akascape
Copy link

Akascape commented Jun 23, 2023

Here is the required code

string = "#0000ff" # the color of background frame
if not string.strartswith("#"): return # return if hex color is not used

converted_color = f"{string[5:7]}{string[3:5]}{string[1:3]}" #rgb->bgr
header_color = ctypes.wintypes.DWORD(int(converted_color, base=16)) 
ctypes.windll.dwmapi.DwmSetWindowAttribute(windll.user32.GetParent(self.winfo_id()), 35, byref(c_int(header_color)), sizeof(c_int(2)))

@Akascape
Copy link

@littlewhitecloud If you wish, you can also use pywinstyles to change the header color easily.

@littlewhitecloud
Copy link
Owner Author

Thank you very much @Akascape ! This maybe a temp solution. Maybe you didn't understand what I said, Actually, I want to get rid of the 6px white frame.

@littlewhitecloud
Copy link
Owner Author

I tried a lot of way but I still can't get rid of it.

@littlewhitecloud littlewhitecloud changed the title Withdraw this white frame? Get rid of the 6px white frame caused by WS_THICKFRAME Jun 24, 2023
@Akascape
Copy link

@littlewhitecloud
But I am not getting that 6 px white frame.

@littlewhitecloud
Copy link
Owner Author

@littlewhitecloud But I am not getting that 6 px white frame.

?

@littlewhitecloud
Copy link
Owner Author

littlewhitecloud commented Jun 24, 2023

image

@littlewhitecloud
Copy link
Owner Author

littlewhitecloud commented Jun 24, 2023

@littlewhitecloud But I am not getting that 6 px white frame.

Oh I mean it will increase more 6px.

@Akascape
Copy link

i am not getting that in windows 11. Am I doing something wrong?

@littlewhitecloud
Copy link
Owner Author

littlewhitecloud commented Jun 24, 2023

i am not getting that in windows 11. Am I doing something wrong?

Can you give a screenshot?
And delete this line:

windll.dwmapi.DwmSetWindowAttribute(
windll.user32.GetParent(self.winfo_id()), 20, byref(c_int(2)), sizeof(c_int(2))
)

@Akascape
Copy link

@littlewhitecloud That white border is the header color. If you remove that line it will show that white header.
Screenshot

But if you use this method mentioned here: #13 (comment)
Then we can change that color.

Screenshot2

This will only work in windows 11.
For windows 10, you have to remove that width somehow,

@Akascape
Copy link

Akascape commented Jun 24, 2023

@littlewhitecloud Can we adjust that titlebar offset in y ?
That offset is useful in windows 11 as the round corners are there, we can then change the color of it.
In windows 10, we can simply set that offset to 0. (detect win10 using sys.getwindowsversion().build < 22523)

@littlewhitecloud
Copy link
Owner Author

littlewhitecloud commented Jun 24, 2023

@littlewhitecloud Can we adjust that titlebar offset in y ? That offset is useful in windows 11 as the round corners are there, we can then change the color of it. In windows 10, we can simply set that offset to 0. (detect win10 using sys.getwindowsversion().build < 22523)

I am looking for how to get rid of it. But how to adjust that titlebar offset in y?

@littlewhitecloud
Copy link
Owner Author

@Akascape Do you know how to use pywin32 to get the messages("MSG") of the windows?

@Akascape
Copy link

Akascape commented Jul 6, 2023

@littlewhitecloud

Do you know how to use pywin32 to get the messages("MSG") of the windows?

No idea about it. I just know how to change window styles and titlebar colors using ctypes.

@littlewhitecloud
Copy link
Owner Author

I found a way that how to remove the white frame, I can get the message and check if the message is WM_NCCALCSIZE. If that then I can get the message and adjust the window

@littlewhitecloud
Copy link
Owner Author

littlewhitecloud commented Jul 7, 2023

But now the problem is I can’t get the message (even use C++ win32)

@Akascape
Copy link

Akascape commented Jul 8, 2023

@littlewhitecloud
Copy link
Owner Author

@Akascape Thanks! Would you like to take part in this project? (It is laggy lol)
My plan is:

  • Work out how to get the message.
  • And try to do something with it.
  • Finally the target window will get rid off the annoying white frame!

@littlewhitecloud
Copy link
Owner Author

I also find out that the task manager has the same white frame (with the mica effect) [I wrote the configuration myself and installed a new computer with Windows11 platform]
image

@Akascape
Copy link

Akascape commented Aug 9, 2023

@littlewhitecloud

Thanks! Would you like to take part in this project? (It is laggy lol)

I wish I can, but I will be very busy in the next months.

@littlewhitecloud
Copy link
Owner Author

It was successed in pywin32, but failed in tkinter.

@littlewhitecloud
Copy link
Owner Author

littlewhitecloud commented Dec 20, 2023

image
!!!!!!!!!?!?!?!?!?!??!

@Akascape @HuyHung1408

@Akascape
Copy link

@littlewhitecloud Very good, so it is fixed ig.

@littlewhitecloud
Copy link
Owner Author

littlewhitecloud commented Dec 20, 2023

I uses a lot of magic from windnd:

"""A special window for custom titlebar"""
from ctypes import c_char_p, windll, WINFUNCTYPE, c_uint64
from pathlib import Path
from tkinter import FLAT, LEFT, RIGHT, TOP, Button, Frame, Label, Menu, Tk, X, Y
from ctypes.wintypes import HWND, MSG, WPARAM, LPARAM
from darkdetect import isDark
from PIL import Image, ImageTk
from data import *

env = Path(__file__).parent
try:
    plugin = windll.LoadLibrary(str(env / "plugin.dll"))
except OSError:  # 32 bit
    plugin = windll.LoadLibrary(str(env / "plugin32.dll"))


class CTT(Tk):
    """A class for custom titlebar"""

    def __init__(self, theme: str = "followsystem", unlimit: bool = False):
        """Class initialiser"""
        super().__init__()

        self.colors = {
            "light": "#ffffff",
            "light_nf": "#f2efef",
            "dark": "#000000",
            "dark_nf": "#2b2b2b",
            "dark_bg": "#202020",
            "button_dark_activefg": "#1a1a1a",
            "button_light_activefg": "#e5e5e5",
            "lightexit_bg": "#f1707a",
            "darkexit_bg": "#8b0a14",
            "exit_fg": "#e81123",
        }
        path = env / "asset"
        if theme == "followsystem":
            if isDark():
                path /= "dark"
                self.settheme("dark")
            else:
                path /= "light"
                self.settheme("light")
        else:
            path /= theme
            self.settheme(theme)

        self.close_load = Image.open(path / "close_50.png")
        self.close_hov_load = Image.open(path / "close_100.png")
        self.min_load = Image.open(path / "minisize_50.png")
        self.min_hov_load = Image.open(path / "minisize_100.png")
        self.full_load = Image.open(path / "fullwin_50.png")
        self.full_hov_load = Image.open(path / "fullwin_100.png")
        self.max_load = Image.open(path / "togglefull_50.png")
        self.max_hov_load = Image.open(path / "togglefull_100.png")

        self.close_img = ImageTk.PhotoImage(self.close_load)
        self.close_hov_img = ImageTk.PhotoImage(self.close_hov_load)
        self.min_img = ImageTk.PhotoImage(self.min_load)
        self.min_hov_img = ImageTk.PhotoImage(self.min_hov_load)
        self.full_img = ImageTk.PhotoImage(self.full_load)
        self.full_hov_img = ImageTk.PhotoImage(self.full_hov_load)
        self.max_img = ImageTk.PhotoImage(self.max_load)
        self.max_hov_img = ImageTk.PhotoImage(self.max_hov_load)

        self.width, self.height = 265, 320
        self.o_m = self.o_f = False
        self.unlimit = unlimit

        self.popup = Menu(self, tearoff=0)
        self.popup.add_command(label="Restore", command=self.resize)
        self.popup.add_command(label="Minsize", command=self.minsize)
        self.popup.add_command(label="Maxsize", command=self.maxsize)
        self.popup.add_separator()
        self.popup.add_command(label="Close (Alt+F4)", command=self.destroy)
        self.popup.entryconfig("Restore", state="disabled")

        self.titlebar = Frame(self, bg=self.bg, height=30)
        self._titleicon = Label(self.titlebar, bg=self.bg)
        self._titletext = Label(self.titlebar, bg=self.bg, fg=self.colors[self.fg])
        self._titlemin = Button(self.titlebar, bg=self.bg)
        self._titlemax = Button(self.titlebar, bg=self.bg)
        self._titleexit = Button(self.titlebar, bg=self.bg)

        self._titleexit.config(
            bd=0,
            activebackground=self.colors["%sexit_bg" % self.theme],
            width=44,
            image=self.close_hov_img,
            relief=FLAT,
            command=self.quit,
        )
        self._titlemin.config(
            bd=0,
            activebackground=self.colors["button_%s_activefg" % self.theme],
            width=44,
            image=self.min_hov_img,
            relief=FLAT,
            command=self.minsize,
        )
        self._titlemax.config(
            bd=0,
            activebackground=self.colors["button_%s_activefg" % self.theme],
            width=44,
            image=self.full_hov_img,
            relief=FLAT,
            command=self.maxsize,
        )

        self.bind("<FocusOut>", self.focusout)
        self.bind("<FocusIn>", self.focusin)
        self.bind("<F11>", self.maxsize)

        self._titleexit.bind("<Enter>", self.exit_on_enter)
        self._titleexit.bind("<Leave>", self.exit_on_leave)

        self._titlemax.bind("<Enter>", self.max_grey)
        self._titlemax.bind("<Leave>", self.max_back)
        self._titlemin.bind("<Enter>", self.min_on_enter)
        self._titlemin.bind("<Leave>", self.min_on_leave)
        self._titlemax.bind("<Enter>", self.max_on_enter)
        self._titlemax.bind("<Leave>", self.max_on_leave)

        self._titleicon.bind("<Button-3>", self.popupmenu)
        self._titleicon.bind("<Double-Button-1>", self.close)
        self.titlebar.bind("<ButtonPress-1>", self.dragging)
        self.titlebar.bind("<B1-Motion>", self.moving)
        self.titlebar.bind("<Double-Button-1>", self.maxsize)

        self.setup()

        self._titleicon.pack(fill=Y, side=LEFT, padx=5, pady=5)
        self._titletext.pack(fill=Y, side=LEFT, pady=5)
        self._titleexit.pack(fill=Y, side=RIGHT)
        self._titlemax.pack(fill=Y, side=RIGHT)
        self._titlemin.pack(fill=Y, side=RIGHT)
        self.titlebar.pack(fill=X, side=TOP)
        self.titlebar.pack_propagate(0)

    # Titlebar
    def titlebarconfig(self, color={"color": None, "color_nf": None}, height=30):
        """Config for titlebar"""
        if color["color"] and color["color_nf"]:  # Require two colors : focuson & focusout
            self.bg = color["color"]
            self.nf = color["color_nf"]
            self["background"] = color["color"]

        if self.unlimit:
            self.titlebar["height"] = height
        else:
            if height > 30 and height <= 50:
                self.titlebar["height"] = height

        self.focusout()
        self.focusin()
        self.update()

    # Titlename
    def title(self, text):
        """Rebuild tkinter's title"""
        # TODO: show "..." if title is too long
        self._titletext["text"] = text
        self.wm_title(text)

    def title_grey(self):
        """..."""
        self._titletext["foreground"] = "grey"

    def title_back(self):
        """..."""
        self._titletext["foreground"] = "white"

    def usetitle(self, flag=True):
        """Show / forget titlename"""
        if not flag:
            self._titletext.pack_forget()

    def titleconfig(self, pack="left", font=None):
        """Config the title"""
        self.usetitle(False)
        if pack == "left":
            self._titletext.pack(side=LEFT)
        elif pack == "right":
            self._titletext.pack(side=RIGHT)
        else:
            self._titletext.config(justify="center")
            self._titletext.pack(expand=True)
        if font:
            self._titletext.config(font=font)

    # Titleicon
    def useicon(self, flag=True):
        """Show / forget icon"""
        if not flag:
            self._titleicon.pack_forget()

    def popupmenu(self, event):
        """Popup menu"""
        self.popup.post(event.x_root, event.y_root)

    def loadimage(self, image):
        """Load image"""
        self._icon = Image.open(image)
        self._icon = self._icon.resize((16, 16))
        self._img = ImageTk.PhotoImage(self._icon)
        self._titleicon["image"] = self._img

    def iconphoto(self, image):
        """Rebuild tkinter's iconphoto"""
        self.loadimage(image)
        self.wm_iconphoto(self._img)

    def iconbitmap(self, image):
        """Rebuild tkinter's iconbitmap"""
        self.loadimage(image)
        self.wm_iconbitmap(image)

    # Titlebutton
    def exit_on_enter(self, event=None):
        """..."""
        self._titleexit["background"] = self.colors["exit_fg"]

    def exit_on_leave(self, event=None):
        """..."""
        if not self.o_f:
            self._titleexit["background"] = self.bg
        else:
            self._titleexit["background"] = self.nf

    def exit_grey(self, event=None):
        """..."""
        self._titleexit["image"] = self.close_img

    def exit_back(self, event=None):
        """..."""
        self._titleexit["image"] = self.close_hov_img

    def min_on_enter(self, event=None):
        """..."""
        self._titlemin["background"] = self.colors["button_%s_activefg" % self.theme]

    def min_on_leave(self, event=None):
        """..."""
        self._titlemin["background"] = self.bg

    def min_grey(self, event=None):
        """..."""
        self._titlemin["image"] = self.min_img

    def min_back(self, event=None):
        """..."""
        self._titlemin["image"] = self.min_hov_img

    def max_on_enter(self, event=None):
        """..."""
        self._titlemax["background"] = self.colors["button_%s_activefg" % self.theme]

    def max_on_leave(self, event=None):
        """..."""
        self._titlemax["background"] = self.bg

    def max_grey(self, event=None):
        """..."""
        if not self.o_m:
            self._titlemax["image"] = self.full_img
        else:
            self._titlemax["image"] = self.max_img

    def max_back(self, event=None):
        """..."""
        if not self.o_m:
            self._titlemax["image"] = self.full_hov_img
        else:
            self._titlemax["image"] = self.max_hov_img

    def disabledo(self):
        """..."""
        pass

    def usemaxmin(self, minsize=True, maxsize=True, minshow=True, maxshow=True):
        """Show / Disable min / max button"""
        if not minshow:
            self._titlemin.pack_forget()
        elif not minsize:
            self.min_grey(None)
            self._titlemin["command"] = self.disabledo
            self._titlemin.unbind("<Leave>")
            self._titlemin.unbind("<Enter>")

        if not maxshow:
            self._titlemax.pack_forget()
        elif not maxsize:
            self.max_grey(None)
            self._titlemax["command"] = self.disabledo
            self._titlemax.unbind("<Leave>")
            self._titlemax.unbind("<Enter>")

    # Window functions
    def setup(self):
        """Window Setup"""
        def handle(hwnd, msg, wp, lp):
            if msg == WM_NCCALCSIZE:
                sz = NCCALCSIZE_PARAMS.from_address(lp)
                sz.rgrc[0].top -= 6
            return windll.user32.CallWindowProcW(*map(c_uint64, (globals()[old], hwnd, msg, wp, lp)))

        self.title("CTT")
        self.geometry("%sx%s" % (self.width, self.height))
        self.iconbitmap(env / "asset" / "tk.ico")

        self.hwnd = windll.user32.FindWindowW(c_char_p(None), "CTT")
        plugin.setwindow(self.hwnd)

        limit_num = 200
        for i in range(limit_num):
            if "old_wndproc_%d" % i not in globals():
                old, new = "old_wndproc_%d"%i, "new_wndproc_%d"%i
                break

        prototype = WINFUNCTYPE(c_uint64, c_uint64, c_uint64, c_uint64, c_uint64)

        globals()[old] = None
        globals()[new] = prototype(handle)

        globals()[old] = windll.user32.GetWindowLongPtrA(self.hwnd, GWL_WNDPROC)
        windll.user32.SetWindowLongPtrA(self.hwnd, GWL_WNDPROC, globals()[new])

        self.update()
        self.focus_force()

    def moving(self, event):
        """Window moving"""
        global x, y
        if not self.o_m:
            plugin.move(self.hwnd, self.winfo_x(), self.winfo_y(), event.x - x, event.y - y)  # Use C for speed
        else:
            self.resize()

    def dragging(self, event):
        """Start drag window"""
        global x, y
        x = event.x
        y = event.y

    # TODO: rewrite the maxsize function
    def maxsize(self, event=None):
        """Maxsize Window"""
        if event and self.o_m:
            self.resize()
        else:
            geometry = self.wm_geometry().split("+")[0].split("x")
            self.width, self.height = geometry[0], geometry[1]
            self.popup.entryconfig("Restore", state="active")
            self.popup.entryconfig("Maxsize", state="disabled")
            self.w_x, self.w_y = self.winfo_x(), self.winfo_y()
            self.o_m = True
            self._titlemax["image"] = self.max_hov_img
            self._titlemax["command"] = self.resize
            w, h = self.wm_maxsize()
            self.geometry("%dx%d-1+0" % (w - 14, h - 40))

    def resize(self):
        """Resize window"""
        self.popup.entryconfig("Restore", state="disabled")
        self.popup.entryconfig("Maxsize", state="active")
        self.wm_geometry("%dx%d+%d+%d" % (int(self.width), int(self.height), int(self.w_x), int(self.w_y)))
        self._titlemax["command"] = self.maxsize
        self._titlemax["image"] = self.full_hov_img
        self.o_m = False

    def minsize(self):
        """Minsize window"""
        self.attributes("-alpha", 0)
        self.bind("<FocusIn>", self.deminsize)

    def deminsize(self, event):
        """Deminsize window"""
        self.attributes("-alpha", 1)
        self.bind("<FocusIn>", self.focusin)

    def setcolor(self, status, color):
        if status == "out":
            self.exit_grey()
            self.min_grey()
            self.max_grey()
            self.title_grey()
            self.o_f = True

        else:
            self.exit_back()
            self.min_back()
            self.max_back()
            self.title_back()
            self.o_f = False

            if self.theme == "followsystem" or self.theme == "light":
                self._titletext["fg"] = self.colors[self.fg]

        self.titlebar["bg"] = color
        self._titletext["bg"] = color
        self._titleicon["bg"] = color
        self._titlemin["bg"] = color
        self._titlemax["bg"] = color
        self._titleexit["bg"] = color

    def focusout(self, event=None):
        """When focusout"""
        self.setcolor("out", self.nf)

    def focusin(self, event=None):
        """When focusin"""
        self.setcolor("in", self.bg)

    def close(self, event=None):
        """Close Window"""
        self.destroy()

    def sg(self, w, h):
        """Change the self.w and self.h forcely"""
        self.width, self.height = w, h
        self.geometry("%sx%s" % (self.width, self.height))

    def geometry(self, size):
        """Rebuild tkinter's geometry"""
        if self.width and self.height:
            pass
        else:
            self.width, self.height = size.split("x")[0], size.split("x")[1]
        self.wm_geometry(size)

    def settheme(self, theme):
        """Set the window's theme"""
        if theme == "dark":
            self.theme = "dark"
            self.bg = self.colors["dark"]
            self.nf = self.colors["dark_nf"]
            self.fg = "light"
            self["background"] = self.colors["dark_bg"]

            self.update()
            #windll.dwmapi.DwmSetWindowAttribute(
            #    windll.user32.GetParent(self.winfo_id()), 20, byref(c_int(2)), sizeof(c_int(2))
            #)
            self.update()
        else:
            self.theme = "light"
            self.bg = self.colors["light"]
            self.nf = self.colors["light_nf"]
            self.fg = "dark"
            self.update()

CTT().mainloop()

@littlewhitecloud
Copy link
Owner Author

also data.py

from ctypes.wintypes import HWND, RECT, UINT
from ctypes import POINTER, Structure, c_int

WM_NCCALCSIZE = 0x0083
GWL_WNDPROC = -4

class PWINDOWPOS(Structure):
    _fields_ = [
        ('hWnd',            HWND),
        ('hwndInsertAfter', HWND),
        ('x',               c_int),
        ('y',               c_int),
        ('cx',              c_int),
        ('cy',              c_int),
        ('flags',           UINT)
    ]


class NCCALCSIZE_PARAMS(Structure):
    _fields_ = [
        ('rgrc', RECT*3),
        ('lppos', POINTER(PWINDOWPOS))
    ]

@Akascape
Copy link

Akascape commented Dec 20, 2023

@littlewhitecloud Also add the ability to change the title bar color, header text color

@littlewhitecloud
Copy link
Owner Author

I will do it, but we just need to change the color of the frame.
the 6px frame already gone

@HuyHung1408
Copy link

wow nice work @littlewhitecloud!

@littlewhitecloud
Copy link
Owner Author

wow nice work @littlewhitecloud!

Thanks!

@littlewhitecloud
Copy link
Owner Author

Will fix it in the rebuild!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
✨ enhancement New feature or request 📗 help wanted Extra attention is needed ❌ invalid This doesn't seem right
Projects
None yet
Development

No branches or pull requests

3 participants