Skip to content

Instantly share code, notes, and snippets.

@59de44955ebd
Last active September 29, 2024 15:32
Show Gist options
  • Save 59de44955ebd/ed6e721c3350fe16a97dbcfb4c9378bf to your computer and use it in GitHub Desktop.
Save 59de44955ebd/ed6e721c3350fe16a97dbcfb4c9378bf to your computer and use it in GitHub Desktop.
Minimal demo for native printing (with page setup and print dialog) with Python+Tk in Windows
from tkinter import Tk, messagebox, scrolledtext, Menu, END, BOTH
from ctypes import windll, byref, sizeof, Structure, POINTER
from ctypes.wintypes import (BOOL, DWORD, HDC, HGLOBAL, HINSTANCE, HWND,
INT, LONG, LPARAM, LPCWSTR, LPVOID, POINT, RECT, UINT, WORD)
########################################
# Used constants
########################################
DT_CALCRECT = 1024
LOGPIXELSX = 88
LOGPIXELSY = 90
PD_NOSELECTION = 4
PD_PAGENUMS = 2
PD_RETURNDC = 256
PD_USEDEVMODECOPIES = 262144
PSD_DISABLEORIENTATION = 256
PSD_INHUNDREDTHSOFMILLIMETERS = 8
PSD_MARGINS = 2
INCHES_PER_UNIT = 1 / 2540 # inch is 25.4 mm, our unit is hundredth of mm
########################################
# Used structures
########################################
class DOCINFOW(Structure):
def __init__(self, *args, **kwargs):
super(DOCINFOW, self).__init__(*args, **kwargs)
self.cbSize = sizeof(self)
_fields_ = [
('cbSize', INT),
('lpszDocName', LPCWSTR),
('lpszOutput', LPCWSTR),
('lpszDatatype', LPCWSTR),
('fwType', DWORD),
]
class PAGESETUPDLGW(Structure):
def __init__(self, *args, **kwargs):
super(PAGESETUPDLGW, self).__init__(*args, **kwargs)
self.lStructSize = sizeof(self)
_fields_ = [
('lStructSize', DWORD),
('hwndOwner', HWND),
('hDevMode', HGLOBAL),
('hDevNames', HGLOBAL),
('Flags', DWORD),
('ptPaperSize', POINT),
('rtMinMargin', RECT),
('rtMargin', RECT),
('hInstance', HINSTANCE),
('lCustData', LPARAM),
('lpfnPageSetupHook', LPVOID), # LPPAGESETUPHOOK, unused
('lpfnPagePaintHook', LPVOID), # LPPAGEPAINTHOOK, unused
('lpPageSetupTemplateName', LPCWSTR),
('hPageSetupTemplate', HGLOBAL),
]
class PRINTDLGW(Structure):
def __init__(self, *args, **kwargs):
super(PRINTDLGW, self).__init__(*args, **kwargs)
self.lStructSize = sizeof(self)
_fields_ = [
('lStructSize', DWORD),
('hwndOwner', HWND),
('hDevMode', HGLOBAL),
('hDevNames', HGLOBAL),
('hDC', HDC),
('Flags', DWORD),
('nFromPage', WORD),
('nToPage', WORD),
('nMinPage', WORD),
('nMaxPage', WORD),
('nCopies', WORD),
('hInstance', HINSTANCE),
('lCustData', LPARAM),
('lpfnPrintHook', LPVOID), # LPPRINTHOOKPROC, unused
('lpfnSetupHook', LPVOID), # LPSETUPHOOKPROC, unused
('lpPrintTemplateName', LPCWSTR),
('lpSetupTemplateName', LPCWSTR),
('hPrintTemplate', HGLOBAL),
('hSetupTemplate', HGLOBAL),
]
########################################
# Used winapi functions
########################################
comdlg32 = windll.comdlg32
comdlg32.PageSetupDlgW.argtypes = (POINTER(PAGESETUPDLGW),)
comdlg32.PrintDlgW.argtypes = (POINTER(PRINTDLGW),)
gdi32 = windll.Gdi32
gdi32.GetDeviceCaps.argtypes = (HDC, INT)
gdi32.StartDocW.argtypes = (HDC, POINTER(DOCINFOW))
gdi32.EndDoc.argtypes = (HDC,)
gdi32.AbortDoc.argtypes = (HDC,)
gdi32.StartPage.argtypes = (HDC,)
gdi32.EndPage.argtypes = (HDC,)
user32 = windll.user32
user32.DrawTextW.argtypes = (HDC, LPCWSTR, INT, POINTER(RECT), UINT)
class Main(Tk):
def __init__(self):
super().__init__()
self.title('Tk Print Demo')
# defaults, in mm/100
self.print_paper_size = POINT(21000, 29700) # Din A4 (for Europe)
self.print_margins = RECT(1000, 1500, 1000, 1500)
menu_bar = Menu(self)
self.config(menu=menu_bar)
file_menu = Menu(menu_bar, tearoff=False)
menu_bar.add_cascade(label='File', menu=file_menu)
file_menu.add_command(label='Page Setup...', command=self.page_setup)
file_menu.add_command(label='Print...', command=self.do_print)
file_menu.add_separator()
file_menu.add_command(label='Exit', command=self.destroy)
help_menu = Menu(menu_bar, tearoff=False)
menu_bar.add_cascade(label='Help', menu=help_menu)
help_menu.add_command(label='About', command=self.about)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.text_widget = scrolledtext.ScrolledText(self)
self.text_widget.pack(fill=BOTH, expand=True)
for i in range(301):
self.text_widget.insert(END,
f'{i:>3} Hello World! Hello World! Hello World! Hello World! Hello World!\n')
def about(self):
messagebox.showinfo('About', 'A minimal Tk print demo for Windows')
def page_setup(self):
psd = PAGESETUPDLGW()
psd.hwndOwner = int(self.frame(), 16)
psd.Flags = PSD_INHUNDREDTHSOFMILLIMETERS | PSD_MARGINS | PSD_DISABLEORIENTATION
psd.ptPaperSize = self.print_paper_size
psd.rtMargin = self.print_margins
if comdlg32.PageSetupDlgW(byref(psd)):
self.print_paper_size = psd.ptPaperSize
self.print_margins = psd.rtMargin
def do_print(self, doc_name='Document'):
pdlg = PRINTDLGW()
pdlg.Flags = PD_RETURNDC | PD_USEDEVMODECOPIES | PD_NOSELECTION
pdlg.nFromPage = 1
pdlg.nToPage = 1
pdlg.nMinPage = 1
pdlg.nMaxPage = 0xffff
if not comdlg32.PrintDlgW(byref(pdlg)):
return False
hdc = pdlg.hDC
di = DOCINFOW()
di.lpszDocName = doc_name
job_id = gdi32.StartDocW(hdc, byref(di))
if job_id <= 0:
return False
# Get printer resolution
pt_dpi = POINT()
pt_dpi.x = gdi32.GetDeviceCaps(hdc, LOGPIXELSX)
pt_dpi.y = gdi32.GetDeviceCaps(hdc, LOGPIXELSY)
# Set page rect in logical units, rcPage is the rectangle {0, 0, maxX, maxY} where
# maxX+1 and maxY+1 are the number of physically printable pixels in x and y.
# rc is the rectangle to render the text in (which will, of course, fit within the
# rectangle defined by rcPage).
rcPage = RECT()
rcPage.top = 0
rcPage.left = 0
rcPage.right = round(self.print_paper_size.x * pt_dpi.x * INCHES_PER_UNIT)
rcPage.bottom = round(self.print_paper_size.y * pt_dpi.x * INCHES_PER_UNIT)
rc = RECT()
rc.left = round(self.print_margins.left * pt_dpi.x * INCHES_PER_UNIT)
rc.top = round(self.print_margins.top * pt_dpi.x * INCHES_PER_UNIT)
rc.right = rcPage.right - round(self.print_margins.right * pt_dpi.x * INCHES_PER_UNIT)
rc.bottom = rcPage.bottom - round(self.print_margins.bottom * pt_dpi.x * INCHES_PER_UNIT)
# calculate line height (quick & dirty)
success = user32.DrawTextW(hdc, 'Whatever', -1, byref(rc),
DT_CALCRECT) > 0
if not success:
return False
line_height = rc.bottom - rc.top
rc.right = rcPage.right - round(self.print_margins.right * pt_dpi.x * INCHES_PER_UNIT)
rc.bottom = rcPage.bottom - round(self.print_margins.bottom * pt_dpi.x * INCHES_PER_UNIT)
lines_per_page = (rc.bottom - rc.top) // line_height
lines = self.text_widget.get('1.0', END).split('\n')
if pdlg.Flags & PD_PAGENUMS:
first_page, last_page = pdlg.nFromPage, pdlg.nToPage
else:
first_page, last_page = 1, 0xffffffff
# print successive pages
page = 0
for i in range(0, len(lines), lines_per_page):
page += 1
if page < first_page:
continue
success = gdi32.StartPage(hdc) > 0
if not success:
break
success = user32.DrawTextW(hdc, '\n'.join(lines[i:i + lines_per_page]),
-1, byref(rc), 0) > 0
if not success:
break
gdi32.EndPage(hdc)
if page >= last_page:
break
if success:
gdi32.EndDoc(hdc)
else:
gdi32.AbortDoc(hdc)
return success
if __name__ == '__main__':
main = Main()
main.mainloop()
@59de44955ebd
Copy link
Author

Screenshot (Python 3.12/Windows 11/German)
screenshot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment