Skip to content

Commit

Permalink
Merge pull request #44 from Zingzy/42-bug-when-the-feature-to-hide-th…
Browse files Browse the repository at this point in the history
…e-taskbar-is-invoked-the-size-of-the-window-is-modified

Add dynamic height adjustment to hide_titlebar method
Add documentation for the new `no_span` parameter
  • Loading branch information
Zingzy authored Dec 31, 2024
2 parents 79c2f36 + f209901 commit a12493c
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 22 deletions.
152 changes: 148 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ https://github.com/Zingzy/hPyT/assets/90309290/f86df1c7-b75b-4477-974a-eb34cc117
- [📚 Supported Libraries](#-supported-libraries)
- [📦 Installing](#-installing)
- [📥 Importing](#-importing)
- [Hide/Unhide Title Bar](#hideunhide-title-bar)
- [Hide/Unhide TitleBar](#hideunhide-titlebar)
- [**Understanding Window Geometry**](#understanding-window-geometry)
- [**Impact of Hiding the Title Bar**](#impact-of-hiding-the-title-bar)
- [**Potential Issues**](#potential-issues)
- [**Solution**](#solution)
- [Example Usage](#example-usage)
- [Comparision of the dimensions with and without `no_span=True`:](#comparision-of-the-dimensions-with-and-without-no_spantrue)
- [Visual Example:](#visual-example)
- [🌈 Rainbow TitleBar](#-rainbow-titlebar)
- [🌈 Rainbow Border](#-rainbow-border)
- [🔄 Synchronizing the Rainbow Effect with other elements](#-synchronizing-the-rainbow-effect-with-other-elements)
Expand Down Expand Up @@ -87,14 +94,151 @@ from customtkinter import * # you can use any other library from the above menti
window = CTk() # creating a window using CustomTkinter
```

## Hide/Unhide Title Bar
## Hide/Unhide TitleBar

```python
title_bar.hide(window) # hides full titlebar
title_bar.hide(window, no_span = False) # hides full titlebar
# optional parameter : no_span, more details in the note below
# title_bar.unhide(window)
```

![image](https://github.com/littlewhitecloud/hPyT/assets/71159641/03e533fe-c42a-4d84-b138-176a73ad7977)
![Hide Titlebar preview](https://github.com/littlewhitecloud/hPyT/assets/71159641/03e533fe-c42a-4d84-b138-176a73ad7977)

<details>
<summary><h3>❗Important Note when hiding the titlebar</h3></summary>

When hiding a title bar, the application window's total geometry and its content area geometry behave differently, which may introduce ambiguities. Here's a detailed explanation of the issue:

#### **Understanding Window Geometry**

1. **Full Window Dimensions**:
- Includes the content area, title bar, and borders.
- When the user specifies dimensions (e.g., `400x400`), it usually represents the **content area dimensions**. The total window height becomes `content height + title bar height + border width`.
- The color of the `top border` and `title bar` is usually the same, making it appear as a single entity.

2. **Content Area Dimensions**:
- Represents only the usable area inside the window, excluding the title bar and borders.

#### **Impact of Hiding the Title Bar**

When the title bar is hidden:
- The **content area height** expands to occupy the height previously used by the title bar. For example, a `400x400` content area might expand to `400x438` (assuming the visual title bar height is 38px).

Better illustrated in the following example:

```py
...

def show_window_dimensions():
hwnd: int = ctypes.windll.user32.GetForegroundWindow()

x_with_decorations: int = root.winfo_rootx() # X position of the full window
y_with_decorations: int = root.winfo_rooty() # Y position of the full window

x_without_decorations: int = root.winfo_x() # X position of the content area
y_without_decorations: int = root.winfo_y() # Y position of the content area

titlebar_height: int = y_with_decorations - y_without_decorations
border_width: int = x_with_decorations - x_without_decorations

window_rect: RECT = get_window_rect(hwnd)

width: int = window_rect.right - window_rect.left
height: int = window_rect.bottom - window_rect.top

print(f"Title bar height: {titlebar_height}")
print(f"Border width: {border_width}")
print(f"Main window dimensions: {width}x{height}")
print(
f"Content window dimensions: {root.winfo_geometry()}"
) # This will return the dimensions of the content area only

...

def click(e=None):
root.update_idletasks()

print("------ Before hiding title bar ------")
show_window_dimensions()

title_bar.hide(root)
is_hidden = True

print("------ After hiding title bar ------")
show_window_dimensions()


button = CTkButton(root, text="Click Me", command=click)
button.place(relx=0.5, rely=0.5, anchor="center")

root.mainloop()
```

Output:

```cmd
------ Before hiding title bar ------
Title bar height: 38
Border width: 9
Main window dimensions: 468x497
Content window dimensions: 450x450
------ After hiding title bar ------
Title bar height: 0
Border width: 9
Main window dimensions: 468x497
Content window dimensions: 450x488
```

By the above example, you can see that the content area height has increased from `450px` to `488px` after hiding the title bar.

#### **Potential Issues**
This automatic resizing may cause layout problems or unintended behavior in some applications. For instance:
- UI elements might **overlap** or **stretch**.
- Custom layouts may require recalibration.

#### **Solution**
To address this, a `no_span` parameter is introduced in the `hide` method. This parameter allows users to control whether the content area height should be adjusted dynamically to maintain its original size.

- **Default Behavior (`no_span=False`)**:
The content area height will expand to occupy the title bar's space.
- **With `no_span=True`**:
The content area will be resized dynamically to maintain its original dimensions.

### Example Usage

```python
title_bar.hide(root, no_span=True)
```

#### Comparision of the dimensions with and without `no_span=True`:

```diff
- Content window dimensions: 450x488
+ Content window dimensions: 450x450

- Main window dimensions: 468x497
+ Main window dimensions: 468x459
```
#### Visual Example:

<table align="center">
<thead>
<tr>
<th><code>no_span = False</code></th>
<th><code>no_span = True</code></th>
</tr>
</thead>
<tbody>
<tr>
<td align="center"><img src="https://raw.githubusercontent.com/Zingzy/hPyT/main/assets/span.gif" alt="Height of the Content area changes when the no_span paramer is set to False by default" width=300></td>
<td align="center"><img src="https://raw.githubusercontent.com/Zingzy/hPyT/main/assets/no_span.gif" alt="Height of the Content area does not change when the no_span paramer is set to False by default" width=300></td>
</tr>
</tbody>
</table>

---

</details>

## 🌈 Rainbow TitleBar

Expand Down
Binary file added assets/no_span.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/span.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
86 changes: 68 additions & 18 deletions hPyT/hPyT.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import math
import threading
import time
from typing import Any, Tuple, Union, List
from typing import Any, Tuple, Union, List, Dict

try:
import ctypes
Expand All @@ -26,6 +26,7 @@
WS_MAXIMIZEBOX = 0x00010000
WS_CAPTION = 0x00C00000
WS_SYSMENU = 0x00080000
WS_BORDER = 0x00800000

WS_EX_LAYERED = 524288

Expand All @@ -35,8 +36,6 @@
WM_NCACTIVATE = 0x0086
WM_NCPAINT = 0x0085

TITLE_BAR_HEIGHT_REDUCTION = 7

SWP_NOZORDER = 4
SWP_NOMOVE = 2
SWP_NOSIZE = 1
Expand Down Expand Up @@ -88,27 +87,30 @@ class NCCALCSIZE_PARAMS(ctypes.Structure):
class title_bar:
"""Hide or unhide the title bar of a window."""

_height_reduction: Dict[
int, int
] = {} # To track the height reduction applied for each window

@classmethod
def hide(cls, window: Any) -> None:
def hide(cls, window: Any, no_span: bool = False) -> None:
"""
Hide the title bar of the specified window.
Args:
window (object): The window object to modify (e.g., a Tk instance in Tkinter).
no_span (bool): Whether to resize the window to fit the content area only. Default is False.
"""

def handle(hwnd: int, msg: int, wp: int, lp: int) -> int:
if msg == WM_NCCALCSIZE and wp:
# Adjust the non-client area (title bar) size
# Here we are basically removing the top border (because the title bar in windows is made of 2 components: the actual titlebar and the border, both having same color)
lpncsp = NCCALCSIZE_PARAMS.from_address(lp)
lpncsp.rgrc[
0
].top -= (
TITLE_BAR_HEIGHT_REDUCTION # Reduce the height of the title bar
)
lpncsp.rgrc[0].top -= border_width # Reduce the height of the title bar

elif msg in [WM_NCACTIVATE, WM_NCPAINT]:
# Prevent Windows from drawing the title bar when the window is activated or painted
# Here we are telling windows not to draw border when the window is switched
return 1 # Tell Windows not to process further

# Default processing for other messages
Expand All @@ -127,15 +129,45 @@ def handle(hwnd: int, msg: int, wp: int, lp: int) -> int:

hwnd: int = module_find(window)

# Get the window and client dimensions
rect = RECT()
client_rect = RECT()
ctypes.windll.user32.GetWindowRect(hwnd, ctypes.byref(rect))
ctypes.windll.user32.GetClientRect(hwnd, ctypes.byref(client_rect))

full_width: int = rect.right - rect.left
full_height: int = rect.bottom - rect.top
client_width: int = client_rect.right
client_height: int = client_rect.bottom

# Calculate the border width and title bar height
border_width: int = (full_width - client_width) // 2
title_bar_height: int = full_height - client_height - border_width

# Override the window procedure if not already done
if globals().get(old) is None:
globals()[old] = get_window_long(hwnd, GWL_WNDPROC)

globals()[new] = prototype(handle)
set_window_long(hwnd, GWL_WNDPROC, globals()[new])

old_style = get_window_long(hwnd, GWL_STYLE)
new_style = old_style & ~WS_CAPTION
new_style = (old_style & ~WS_CAPTION) | WS_BORDER
set_window_long(hwnd, GWL_STYLE, new_style)

if no_span:
cls._height_reduction[hwnd] = title_bar_height
set_window_pos(
hwnd,
0,
0,
0,
full_width,
full_height - title_bar_height,
SWP_NOZORDER | SWP_NOMOVE,
)
return

set_window_pos(
hwnd,
0,
Expand All @@ -161,9 +193,27 @@ def unhide(cls, window: Any) -> None:
set_window_long(hwnd, GWL_WNDPROC, globals()["old_wndproc"])
globals()["old_wndproc"] = None

# Restore the original height if no_span was used
height_reduction: int = cls._height_reduction.pop(hwnd, 0)

old_style = get_window_long(hwnd, GWL_STYLE)
new_style = old_style | WS_CAPTION
set_window_long(hwnd, GWL_STYLE, new_style)

if height_reduction:
rect = RECT()
ctypes.windll.user32.GetWindowRect(hwnd, ctypes.byref(rect))
set_window_pos(
hwnd,
0,
0,
0,
rect.right - rect.left,
rect.bottom - rect.top + height_reduction,
SWP_NOZORDER | SWP_NOMOVE,
)
return

set_window_pos(
hwnd,
0,
Expand Down Expand Up @@ -499,12 +549,12 @@ def set_accent(cls, window: Any) -> None:
window (object): The window objec t to modify (e.g., a Tk instance in Tkinter).
"""

def set_titlebar_color_accent(hwnd):
old_accent = (-1, -1, -1)
def set_titlebar_color_accent(hwnd) -> None:
old_accent: str = "#000000"

while hwnd in accent_color_titlebars:
if not old_accent == get_accent_color():
color = convert_color(get_accent_color())
if old_accent != get_accent_color():
color: int = convert_color(get_accent_color())
old_ex_style = get_window_long(hwnd, GWL_EXSTYLE)
new_ex_style = old_ex_style | WS_EX_LAYERED
set_window_long(hwnd, GWL_EXSTYLE, new_ex_style)
Expand Down Expand Up @@ -649,8 +699,8 @@ def set_accent(cls, window: Any) -> None:
window (object): The window object to modify (e.g., a Tk instance in Tkinter).
"""

def set_border_color_accent(hwnd):
old_accent = (-1, -1, -1)
def set_border_color_accent(hwnd) -> None:
old_accent: str = "#000000"

while hwnd in accent_color_borders:
if not old_accent == get_accent_color():
Expand Down Expand Up @@ -736,7 +786,7 @@ def start(cls, window: Any, interval: int = 5, color_stops: int = 5) -> None:
Higher values for `color_stops` might skip most of the colors defying the purpose of the rainbow effect.
"""

def color_changer(hwnd: int, interval: int):
def color_changer(hwnd: int, interval: int) -> None:
r, g, b = 200, 0, 0
while hwnd in rnbtbs:
cls.current_color = (r << 16) | (g << 8) | b
Expand Down Expand Up @@ -923,7 +973,7 @@ def stop(cls, window: Any) -> None:
window (object): The window object to modify (e.g., a Tk instance in Tkinter).
"""

hwnd = module_find(window)
hwnd: int = module_find(window)
if hwnd in rnbbcs:
rnbbcs.remove(hwnd)
else:
Expand Down

0 comments on commit a12493c

Please sign in to comment.