Sunday, March 07, 2010

IronPython, WPF, and button-free windows

So, how do we achieve something like this with WPF, using IronPython?

Assume Vista or later (if you want to support XP, do something different if System.Environment.OSVersion.Version.Major < 6)

Start with some XAML

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window2" Height="300" Width="400" MinWidth="400" MinHeight="100"  WindowStyle="None" AllowsTransparency="True">
    <Grid>
        <Grid Name="banner">
            <Image Name="icon" Stretch="Fill" HorizontalAlignment="Left" VerticalAlignment="Top" Width="20" Height="20" Margin="4,4,0,0" />
            <Label Height="28" Name="title" VerticalAlignment="Top" Background="WhiteSmoke" Margin="28,0,0,0">Label</Label>
        </Grid>
        <StackPanel Margin="0,28,0,0" Name="body" VerticalAlignment="Top" Height="230">
            <TextBlock Height="100" Name="message" VerticalAlignment="Top" />
            <Image Name="image" Stretch="None" Height="130" Width="400" />
        </StackPanel>
        <StackPanel Name="buttons" Height="55" VerticalAlignment="Bottom" Orientation="Horizontal">
            <!-- Button Height="23" Name="button1" Width="75" Margin="2">Button</Button -->
        </StackPanel>
    </Grid>
</Window>

We can then use ctypes to get at the glass APIs, and set up some re-usable types

from ctypes import *
from System.Windows.Interop import WindowInteropHelper, HwndSource
from System import IntPtr
from System.Windows.Media import Brushes, Colors, Brush
dwmapi = windll.dwmapi
class __MARGINS(Structure):
_fields_ = [("Left", c_int), ("Right", c_int), ("Top", c_int), ("Bottom", c_int)]
def ExtendGlassFrame(window, margin):
val = c_int()
result = dwmapi.DwmIsCompositionEnabled(byref(val))
success = result == 0 and not (val == 0)
if not success:
return False
hwnd = WindowInteropHelper(window).Handle
if (hwnd == IntPtr.Zero):
return False
window.Background = Brushes.Transparent
HwndSource.FromHwnd(hwnd).CompositionTarget.BackgroundColor = Colors.Transparent
margins = __MARGINS(int(margin.Left),
int(margin.Right),
int(margin.Top),
int(margin.Bottom))
result = dwmapi.DwmExtendFrameIntoClientArea(hwnd, byref(margins))
return result == 0
class __GlassHelper:
def __init__(self, w, t):
self.window = w
self.thickness = t
def ApplyGlass(self):
val = ExtendGlassFrame(self.window, self.thickness)
hwnd = WindowInteropHelper(self.window).Handle
HwndSource.FromHwnd(hwnd).AddHook(self.__wndproc)
def __wndproc(self, hwnd, msg, wparam, lparam, handled):
if msg == 0x031E: ## composition changed
handled.Value = True ## set ref argument
ExtendGlassFrame(self.window, self.thickness)
return IntPtr.Zero
view raw gistfile1.py hosted with ❤ by GitHub

which we can then hook into our window (assume we have loaded the XAML into a variable window)

# make the top region glass
window.AllowsTransparency = False
window.Background = Brushes.Transparent
window.FindName('title').Background = Brushes.Transparent
thickness = Thickness(0.0, float(window.FindName('title').Height), 0.0, 0.0)
helper = __GlassHelper(window, thickness)
window.SourceInitialized += lambda s,e : helper.ApplyGlass()
# make the content match what's shown in the taskbar
window.FindName('title').Content = window.Title
window.FindName('icon').Source = window.Icon
# makes the window movable by press and drag
window.MouseLeftButtonDown += lambda s,e : window.DragMove()
view raw gistfile1.py hosted with ❤ by GitHub

Of course you now need to ensure you have some button or other control that will let you dismiss the window, now you no longer have the kiss of death available at top right.

The ref parameter in the WndProc hook is the reason behind the previous post...

No comments :