New version from Timothy Hochberg

git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@24221 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
This commit is contained in:
Robin Dunn
2003-10-18 04:33:35 +00:00
parent 81744507e8
commit 40f5c32cbd

View File

@@ -1,62 +1,109 @@
"""wxFancyText -- methods for rendering XML specified text
"""<font weight="bold" size="16">FancyText</font> -- <font style="italic" size="16">methods for rendering XML specified text</font>
<font family="swiss" size="12">
This module exports four main methods::
<font family="fixed" style="slant">
def GetExtent(str, dc=None, enclose=True)
def GetFullExtent(str, dc=None, enclose=True)
def RenderToBitmap(str, background=None, enclose=True)
def RenderToDC(str, dc, x, y, enclose=True)
</font>
In all cases, 'str' is an XML string. Note that start and end tags
are only required if *enclose* is set to False. In this case the
text should be wrapped in FancyText tags.
This module has four main methods:
In addition, the module exports one class::
<font family="fixed" style="slant">
class StaticFancyText(self, window, id, text, background, ...)
</font>
This class works similar to StaticText except it interprets its text
as FancyText.
def getExtent(str, dc=None, enclose=1):
def renderToBitmap(str, background=None, enclose=1)
def renderToDC(str, dc, x, y, enclose=1)
The text can support<sup>superscripts</sup> and <sub>subscripts</sub>, text
in different <font size="20">sizes</font>, <font color="blue">colors</font>, <font style="italic">styles</font>, <font weight="bold">weights</font> and
<font family="script">families</font>. It also supports a limited set of symbols,
currently <times/>, <infinity/>, <angle/> as well as greek letters in both
upper case (<Alpha/><Beta/>...<Omega/>) and lower case (<alpha/><beta/>...<omega/>).
In all cases, 'str' is an XML string. The tags in the string can
currently specify the font, subscripts, superscripts, and the angle
sign. The allowable properties of font are size, family, style, weght,
encoding, and color. See the example on the bottom for a better idea
of how this works.
We can use doctest/guitest to display this string in all its marked up glory.
<font family="fixed">
>>> frame = wx.Frame(wx.NULL, -1, "FancyText demo", wx.DefaultPosition)
>>> sft = StaticFancyText(frame, -1, __doc__, wx.Brush("light grey", wx.SOLID))
>>> frame.SetClientSize(sft.GetSize())
>>> didit = frame.Show()
>>> from guitest import PauseTests; PauseTests()
Note that start and end tags for the string are provided if enclose is
True, so for instance, renderToBitmap("X<sub>1</sub>") will work.
</font></font>
The End"""
# Copyright 2001-2003 Timothy Hochberg
# Use as you see fit. No warantees, I cannot be help responsible, etc.
import copy
import math
import sys
import wx
import xml.parsers.expat
"""
# Copyright 2001 Timothy Hochberg
# Use as you see fit. No warantees, I cannot be held responsible, etc.
__all__ = "GetExtent", "GetFullExtent", "RenderToBitmap", "RenderToDC", "StaticFancyText"
if sys.platform == "win32":
_greekEncoding = str(wx.FONTENCODING_CP1253)
else:
_greekEncoding = str(wx.FONTENCODING_ISO8859_7)
# TODO: Make a wxFancyTextCtrl class that derives from wxControl.
# Add support for line breaks
# etc.
# - Robin
from wxPython.wx import *
import xml.parsers.expat, copy
_families = {"default" : wxDEFAULT, "decorative" : wxDECORATIVE, "roman" : wxROMAN,
"swiss" : wxSWISS, "modern" : wxMODERN}
_styles = {"normal" : wxNORMAL, "slant" : wxSLANT, "italic" : wxITALIC}
_weights = {"normal" : wxNORMAL, "light" : wxLIGHT, "bold" : wxBOLD}
_families = {"fixed" : wx.FIXED, "default" : wx.DEFAULT, "decorative" : wx.DECORATIVE, "roman" : wx.ROMAN,
"script" : wx.SCRIPT, "swiss" : wx.SWISS, "modern" : wx.MODERN}
_styles = {"normal" : wx.NORMAL, "slant" : wx.SLANT, "italic" : wx.ITALIC}
_weights = {"normal" : wx.NORMAL, "light" : wx.LIGHT, "bold" : wx.BOLD}
# The next three classes: Renderer, SizeRenderer and DCRenderer are
# what you will need to override to extend the XML language. All of
# the font stuff as well as the subscript and superscript stuff are in
# the font stuff as well as the subscript and superscript stuff are in
# Renderer.
class Renderer:
_greek_letters = ("alpha", "beta", "gamma", "delta", "epsilon", "zeta",
"eta", "theta", "iota", "kappa", "lambda", "mu", "nu",
"xi", "omnikron", "pi", "rho", "altsigma", "sigma", "tau", "upsilon",
"phi", "chi", "psi", "omega")
defaultSize = wxNORMAL_FONT.GetPointSize()
defaultFamily = wxDEFAULT
defaultStyle = wxNORMAL
defaultWeight = wxNORMAL
defaultEncoding = wxFont_GetDefaultEncoding()
def iround(number):
return int(round(number))
def iceil(number):
return int(math.ceil(number))
class Renderer:
"""Class for rendering XML marked up text.
See the module docstring for a description of the markup.
This class must be subclassed and the methods the methods that do
the drawing overridden for a particular output device.
"""
defaultSize = wx.NORMAL_FONT.GetPointSize()
defaultFamily = wx.DEFAULT
defaultStyle = wx.NORMAL
defaultWeight = wx.NORMAL
defaultEncoding = wx.Font_GetDefaultEncoding()
defaultColor = "black"
def __init__(self, dc=None):
def __init__(self, dc=None, x=0, y=None):
if dc == None:
dc = wxMemoryDC()
dc = wx.MemoryDC()
self.dc = dc
self.offsets = [0]
self.fonts = [{}]
self.width = self.height = 0
self.x = x
self.minY = self.maxY = self._y = y
def getY(self):
if self._y is None:
self.minY = self.maxY = self._y = self.dc.GetTextExtent("M")[1]
return self._y
def setY(self, value):
self._y = y
y = property(getY, setY)
def startElement(self, name, attrs):
method = "start_" + name
if not hasattr(self, method):
@@ -64,16 +111,37 @@ class Renderer:
getattr(self, method)(attrs)
def endElement(self, name):
method = "end_" + name
if not hasattr(self, method):
raise ValueError("XML tag '%s' not supported" % name)
getattr(self, method)()
methname = "end_" + name
if hasattr(self, methname):
getattr(self, methname)()
elif hasattr(self, "start_" + name):
pass
else:
raise ValueError("XML tag '%s' not supported" % methname)
def characterData(self, data):
self.dc.SetFont(self.getCurrentFont())
for i, chunk in enumerate(data.split('\n')):
if i:
self.x = 0
self.y = self.mayY = self.maxY + self.dc.GetTextExtent("M")[1]
if chunk:
width, height, descent, extl = self.dc.GetFullTextExtent(chunk)
self.renderCharacterData(data, iround(self.x), iround(self.y + self.offsets[-1] - height + descent))
else:
width = height = descent = extl = 0
self.updateDims(width, height, descent, extl)
def start_wxFancyString(self, attrs):
pass
def updateDims(self, width, height, descent, externalLeading):
self.x += width
self.width = max(self.x, self.width)
self.minY = min(self.minY, self.y+self.offsets[-1]-height+descent)
self.maxY = max(self.maxY, self.y+self.offsets[-1]+descent)
self.height = self.maxY - self.minY
def end_wxFancyString(self):
def start_FancyText(self, attrs):
pass
start_wxFancyText = start_FancyText # For backward compatibility
def start_font(self, attrs):
for key, value in attrs.items():
@@ -86,7 +154,7 @@ class Renderer:
elif key == "weight":
value = _weights[value]
elif key == "encoding":
pass
value = int(value)
elif key == "color":
pass
else:
@@ -119,11 +187,11 @@ class Renderer:
def end_sup(self):
self.fonts.pop()
self.offsets.pop()
self.offsets.pop()
def getCurrentFont(self):
font = self.fonts[-1]
return wxFont(font.get("size", self.defaultSize),
return wx.TheFontList.FindOrCreateFont(font.get("size", self.defaultSize),
font.get("family", self.defaultFamily),
font.get("style", self.defaultStyle),
font.get("weight", self.defaultWeight),
@@ -131,119 +199,212 @@ class Renderer:
def getCurrentColor(self):
font = self.fonts[-1]
return wxNamedColour(font.get("color", self.defaultColor))
return wx.TheColourDatabase.FindColour(font.get("color", self.defaultColor))
def getCurrentPen(self):
return wx.ThePenList.FindOrCreatePen(self.getCurrentColor(), 1, wx.SOLID)
def renderCharacterData(self, data, x, y):
raise NotImplementedError()
def _addGreek():
alpha = 0xE1
Alpha = 0xC1
def end(self):
pass
for i, name in enumerate(_greek_letters):
def start(self, attrs, code=chr(alpha+i)):
self.start_font({"encoding" : _greekEncoding})
self.characterData(code)
self.end_font()
setattr(Renderer, "start_%s" % name, start)
setattr(Renderer, "end_%s" % name, end)
if name == "altsigma":
continue # There is no capital for altsigma
def start(self, attrs, code=chr(Alpha+i)):
self.start_font({"encoding" : _greekEncoding})
self.characterData(code)
self.end_font()
setattr(Renderer, "start_%s" % name.capitalize(), start)
setattr(Renderer, "end_%s" % name.capitalize(), end)
_addGreek()
class SizeRenderer(Renderer):
"""Processes text as if rendering it, but just computes the size."""
def __init__(self, dc=None):
Renderer.__init__(self, dc)
self.width = self.height = 0
self.minY = self.maxY = 0
def characterData(self, data):
self.dc.SetFont(self.getCurrentFont())
width, height = self.dc.GetTextExtent(data)
self.width = self.width + width
self.minY = min(self.minY, self.offsets[-1])
self.maxY = max(self.maxY, self.offsets[-1] + height)
self.height = self.maxY - self.minY
Renderer.__init__(self, dc, 0, 0)
def renderCharacterData(self, data, x, y):
pass
def start_angle(self, attrs):
self.characterData("M")
def end_angle(self):
pass
def start_infinity(self, attrs):
width, height = self.dc.GetTextExtent("M")
width = max(width, 10)
height = max(height, width / 2)
self.updateDims(width, height, 0, 0)
def start_times(self, attrs):
self.characterData("M")
def start_in(self, attrs):
self.characterData("M")
def start_times(self, attrs):
self.characterData("M")
class DCRenderer(Renderer):
"""Renders text to a wxPython device context DC."""
def __init__(self, dc=None, x=0, y=0):
Renderer.__init__(self, dc)
self.x = x
self.y = y
def characterData(self, data):
self.dc.SetFont(self.getCurrentFont())
def renderCharacterData(self, data, x, y):
self.dc.SetTextForeground(self.getCurrentColor())
width, height = self.dc.GetTextExtent(data)
self.dc.DrawText(data, self.x, self.y + self.offsets[-1])
self.x = self.x + width
self.dc.DrawText(data, x, y)
def start_angle(self, attrs):
self.dc.SetFont(self.getCurrentFont())
self.dc.SetPen(wxPen(self.getCurrentColor(), 1))
self.dc.SetPen(self.getCurrentPen())
width, height, descent, leading = self.dc.GetFullTextExtent("M")
y = self.y + self.offsets[-1] + height - descent
self.dc.DrawLine(self.x, y, self.x+width, y)
self.dc.DrawLine(self.x, y, self.x+width, y-width)
self.x = self.x + width
y = self.y + self.offsets[-1]
self.dc.DrawLine(iround(self.x), iround(y),iround( self.x+width), iround(y))
self.dc.DrawLine(iround(self.x), iround(y), iround(self.x+width), iround(y-width))
self.updateDims(width, height, descent, leading)
def end_angle(self):
pass
def start_infinity(self, attrs):
self.dc.SetFont(self.getCurrentFont())
self.dc.SetPen(self.getCurrentPen())
width, height, descent, leading = self.dc.GetFullTextExtent("M")
width = max(width, 10)
height = max(height, width / 2)
self.dc.SetPen(wx.Pen(self.getCurrentColor(), max(1, width/10)))
self.dc.SetBrush(wx.TRANSPARENT_BRUSH)
y = self.y + self.offsets[-1]
r = iround( 0.95 * width / 4)
xc = (2*self.x + width) / 2
yc = iround(y-1.5*r)
self.dc.DrawCircle(xc - r, yc, r)
self.dc.DrawCircle(xc + r, yc, r)
self.updateDims(width, height, 0, 0)
# This is a rendering function that is primarily used internally,
# although it could be used externally if one had overridden the
# Renderer classes.
def renderToRenderer(str, renderer, enclose=1):
if enclose:
str = '<?xml version="1.0"?><wxFancyString>%s</wxFancyString>' % str
p = xml.parsers.expat.ParserCreate()
p.returns_unicode = 0
p.StartElementHandler = renderer.startElement
p.EndElementHandler = renderer.endElement
p.CharacterDataHandler = renderer.characterData
p.Parse(str, 1)
def start_times(self, attrs):
self.dc.SetFont(self.getCurrentFont())
self.dc.SetPen(self.getCurrentPen())
width, height, descent, leading = self.dc.GetFullTextExtent("M")
y = self.y + self.offsets[-1]
width *= 0.8
width = iround(width+.5)
self.dc.SetPen(wx.Pen(self.getCurrentColor(), 1))
self.dc.DrawLine(iround(self.x), iround(y-width), iround(self.x+width-1), iround(y-1))
self.dc.DrawLine(iround(self.x), iround(y-2), iround(self.x+width-1), iround(y-width-1))
self.updateDims(width, height, 0, 0)
def getExtent(str, dc=None, enclose=1):
def RenderToRenderer(str, renderer, enclose=True):
try:
if enclose:
str = '<?xml version="1.0"?><FancyText>%s</FancyText>' % str
p = xml.parsers.expat.ParserCreate()
p.returns_unicode = 0
p.StartElementHandler = renderer.startElement
p.EndElementHandler = renderer.endElement
p.CharacterDataHandler = renderer.characterData
p.Parse(str, 1)
except xml.parsers.expat.error, err:
raise ValueError('error parsing text text "%s": %s' % (str, err))
# Public interface
def GetExtent(str, dc=None, enclose=True):
"Return the extent of str"
renderer = SizeRenderer(dc)
renderToRenderer(str, renderer, enclose)
return wxSize(renderer.width, renderer.height)
RenderToRenderer(str, renderer, enclose)
return iceil(renderer.width), iceil(renderer.height) # XXX round up
# This should probably only be used internally....
def getFullExtent(str, dc=None, enclose=1):
def GetFullExtent(str, dc=None, enclose=True):
renderer = SizeRenderer(dc)
renderToRenderer(str, renderer, enclose)
return renderer.width, renderer.height, -renderer.minY
RenderToRenderer(str, renderer, enclose)
return iceil(renderer.width), iceil(renderer.height), -iceil(renderer.minY) # XXX round up
def renderToBitmap(str, background=None, enclose=1):
def RenderToBitmap(str, background=None, enclose=1):
"Return str rendered on a minumum size bitmap"
dc = wxMemoryDC()
width, height, dy = getFullExtent(str, dc)
bmp = wxEmptyBitmap(width, height)
dc = wx.MemoryDC()
width, height, dy = GetFullExtent(str, dc, enclose)
bmp = wx.EmptyBitmap(width, height)
dc.SelectObject(bmp)
if background is not None:
dc.SetBackground(background)
if background is None:
dc.SetBackground(wx.WHITE_BRUSH)
else:
dc.SetBackground(background)
dc.Clear()
renderer = DCRenderer(dc, y=dy)
dc.BeginDrawing()
renderToRenderer(str, renderer, enclose)
RenderToRenderer(str, renderer, enclose)
dc.EndDrawing()
dc.SelectObject(wxNullBitmap)
dc.SelectObject(wx.NullBitmap)
if background is None:
img = wx.ImageFromBitmap(bmp)
bg = dc.GetBackground().GetColour()
img.SetMaskColour(bg.Red(), bg.Green(), bg.Blue())
bmp = img.ConvertToBitmap()
return bmp
def renderToDC(str, dc, x, y, enclose=1):
def RenderToDC(str, dc, x, y, enclose=1):
"Render str onto a wxDC at (x,y)"
width, height, dy = getFullExtent(str, dc)
width, height, dy = GetFullExtent(str, dc)
renderer = DCRenderer(dc, x, y+dy)
renderToRenderer(str, renderer, enclose)
RenderToRenderer(str, renderer, enclose)
class StaticFancyText(wx.StaticBitmap):
def __init__(self, window, id, text, *args, **kargs):
args = list(args)
kargs.setdefault('name', 'staticFancyText')
if 'background' in kargs:
background = kargs.pop('background')
elif args:
background = args.pop(0)
else:
background = wx.Brush(window.GetBackgroundColour(), wx.SOLID)
bmp = RenderToBitmap(text, background)
wx.StaticBitmap.__init__(self, window, id, bmp, *args, **kargs)
if __name__ == "__main__":
str = ('<font style="italic" family="swiss" color="red" weight="bold" >some |<sup>23</sup> <angle/>text<sub>with <angle/> subscript</sub> </font> some other text'
'<font family="swiss" color="green" size="40">big green text</font>')
ID_EXIT = 102
class myApp(wxApp):
def OnInit(self):
return 1
app = myApp()
frame = wxFrame(NULL, -1, "wxFancyText demo", wxDefaultPosition)
frame.SetClientSize(getExtent(str))
bmp = renderToBitmap(str, wxCYAN_BRUSH)
sb = wxStaticBitmap(frame, -1, bmp)
EVT_MENU(frame, ID_EXIT, frame.Destroy)
frame.Show(1)
# Old names for backward compatibiliry
getExtent = GetExtent
renderToBitmap = RenderToBitmap
renderToDC = RenderToDC
# Test Driver
def test():
app = wx.PyApp()
box = wx.BoxSizer(wx.VERTICAL)
frame = wx.Frame(wx.NULL, -1, "FancyText demo", wx.DefaultPosition)
frame.SetBackgroundColour("light grey")
sft = StaticFancyText(frame, -1, __doc__)
box.Add(sft, 1, wx.EXPAND)
frame.SetSizer(box)
frame.SetAutoLayout(True)
box.Fit(frame)
box.SetSizeHints(frame)
frame.Show()
app.MainLoop()
if __name__ == "__main__":
test()