Saturday, August 22, 2009

Tk 8.5 is better than wxWidgets on Windows

UPDATE: It appears this issue might be fixed in a future release of wxwidgets.

I frequently write computer programs with graphical user interfaces ("GUI"s). I insist that the interfaces look good on Windows, Mac, and Linux computers. By "good", I mean that the widgets (the buttons, sliders, and what-not), look exactly like those found on most other applications developed specifically for that particular platform. For example, buttons and progress bars on Mac must have that clear blue "Aqua" look.

There are several programming tool kits which help to create native-looking user interfaces on multiple platforms. The three platforms I pay particular attention to are Windows, Mac, and Linux. Cross-platform GUI tool kits include wxWidgets, Tk, and Java Swing. This post documents the failure of wxWidgets and Java Swing to respect Windows font sizes.

Look at the following picture to see the failure of wx and Java to respect the Windows font sizes. From left to right, the test programs are in Visual Basic, python/Tk, python/wx, and Java Swing.



wxWidgets looks nice in some cases, but it has some ways to go to support native look and feel on Windows. I am working on several Windows XP systems, on which I routinely select "Large Fonts" in my desktop preferences. wxWidgets does not respect those preferences.

To see the difference, first set extra large fonts on your desktop:

Far click desktop -> Properties -> Appearance -> Font Size -> Extra Large Fonts

Next, write an application using wxWidgets and test whether it respects your font choice. I didn't think so.

If it's any consolation, Java doesn't respect the Windows font size either.

If you want to use a cross-platform widget tool kit, and your definition of "cross-platform" includes Windows, my recommendation is to use Tk 8.5.

The table below summarizes the results for the four test programs I wrote:


















GUI tool kits on Windows
Tool KitNative look-and-feel?Respects font size?
Visual basicNo(!)Yes
Tk 8.5YesYes
wx 2.8.10YesNo
Java 1.6.0YesNo


Below are the test programs I wrote to create the windows shown at the beginning of this post.


  • Visual Basic

    ' "Hello, World!" program in Visual Basic.
    Module Hello
    Sub Main()
    MsgBox("Hello, World! (VB)") ' Display message on computer screen.
    End Sub
    End Module


  • Tk 8.5 (tkinter in python 3.1)

    # Note - requires python 3.1 for ttk 8.5 support
    import tkinter as tk
    import tkinter.ttk as ttk

    root = tk.Tk()
    padding = 10
    panel = ttk.Frame(root, padding=padding).pack()
    label = ttk.Label(panel, text="Hello, World! (Tk)")
    label.pack(padx=padding, pady=padding)
    button = ttk.Button(panel, text="Hello", default="active")
    button.pack(padx=padding, pady=padding)
    root.mainloop()


  • wx 2.8.10 (in python 2.6 with wxpython)

    import wx

    padding = 10
    app = wx.App(0)
    frame = wx.Frame(None, -1, "Hello")
    panel = wx.Panel(frame)
    sizer = wx.BoxSizer(wx.VERTICAL)
    panel.SetSizer(sizer)
    text = wx.StaticText(panel, -1, "Hello, World! (wx)")
    sizer.Add(text, 0, wx.ALL, padding)
    button = wx.Button(panel, -1, "Hello")
    sizer.Add(button, 0, wx.ALL, padding)
    frame.Centre()
    frame.Show(True)
    app.MainLoop()


  • Java swing 1.6.0

    import javax.swing.*;
    import java.awt.Dimension;

    public class HelloWorldFrame extends JFrame
    {
    public static void main(String args[])
    {
    new HelloWorldFrame();
    }
    HelloWorldFrame()
    {
    try {
    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
    } catch(Exception e) {}
    JPanel panel = new JPanel();
    add(panel);
    panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
    panel.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
    JLabel label = new JLabel("Hello, World! (java)");
    panel.add(label);
    panel.add(Box.createRigidArea(new Dimension(0, 10)));
    JButton button = new JButton("Hello");
    panel.add(button);
    pack();
    setVisible(true);
    }
    }


The wx bug tracker has had a couple of bug reports for this problem, one open for five years. Somehow I doubt they are itching to fix this problem.

The Tk source code that sets the windows correctly appears to be near line 418 of file win/tkWinFont.c in the Tk source code:


if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS,
sizeof(ncMetrics), &ncMetrics, 0)) {
CreateNamedSystemLogFont(interp, tkwin, "TkDefaultFont",
&ncMetrics.lfMessageFont);
CreateNamedSystemLogFont(interp, tkwin, "TkHeadingFont",
&ncMetrics.lfMessageFont);
CreateNamedSystemLogFont(interp, tkwin, "TkTextFont",
&ncMetrics.lfMessageFont);
CreateNamedSystemLogFont(interp, tkwin, "TkMenuFont",
&ncMetrics.lfMenuFont);
CreateNamedSystemLogFont(interp, tkwin, "TkTooltipFont",
&ncMetrics.lfStatusFont);
CreateNamedSystemLogFont(interp, tkwin, "TkCaptionFont",
&ncMetrics.lfCaptionFont);
CreateNamedSystemLogFont(interp, tkwin, "TkSmallCaptionFont",
&ncMetrics.lfSmCaptionFont);
}


The wx source code has similar code in a few locations. But it appears that this technique may be only used for menu fonts and message dialog fonts.

The main problem might be that the method wxGetCCDefaultFont() in the wx source code uses SPI_GETINCONTITLELOGFONT instead of SPI_GETNONCLIENTMETRICS.

Microsoft has documentation for the NONCLIENTMETRICS data structure.

Even if the wx authors fix this today, I fear it will be a long time before the change trickles down into a wxPython release.

Saturday, August 15, 2009

Write your own stereoscopic 3D program using nVidia's "consumer" stereo driver


I have always been a fan of nVidia graphics boards because of their support for 3D stereoscopic games. But the "consumer level" (non-Quadro) stereoscopic drivers only seem to work with games. I have always wondered how to create my own applications that can use the stereoscopic drivers on less-expensive gaming video boards. Now I have found a way.

The "consumer" stereoscopic driver from nVidia only works with "full screen" games. When I started experimenting with OpenGL, I assumed that using the call "glutFullScreen()" might be enough to get the stereoscopic drivers to kick in. But it is not.

The trick is to use the glutEnterGameMode() call. I did a lot of searching on the internet, and nowhere is it mentioned that you must call glutEnterGameMode() to get the nVidia "consumer level" stereoscopic drivers to work. That is why I am sharing this blog post.

My working system is on Windows XP. I am uncertain if this approach will work with Windows Vista/7. I am a bit concerned because nVidia seems to be selling a hardware stereoscopic product these days. I am worried that my custom stereoscopic theater, which uses a pair of polarized video projectors, won't work if I upgrade my Windows version.

Here is how you can do it too, on Windows XP:
  1. Ensure you have a supported nVidia graphics board in your computer. See the stereoscopic driver users' guide for more details.
  2. Get the stereoscopic driver from nVidia. The most recent version (91.31) released for Windows XP is from 2006. That is the one I am using. Consult this driver guide for more details.
  3. Install Python 2.6 and PyOpenGL version 3.0.0, so you can conveniently create OpenGL programs in python.
  4. Familiarize yourself with OpenGL programming. I got started by following the examples of the "red book", the OpenGL Programming Guide.
  5. Study my example program, below, to learn how to call glutGameModeString() and glutEnterGameMode().
Below is the text of a complete working python program that works with the nVidia "consumer level" stereoscopic driver on my Windows XP computer. (The stereoscopic presentation only appears in the full screen gaming mode):

Modify the display() method and the animate() method to show whatever you want!

#!/cygdrive/c/Python26/python

from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
import sys


def do_nothing(*args):
"""
Empty method for glutDisplayFunc during risky transition to game mode.
"""
pass


class HelloOpenGL(object):
"""
Creates a rotating wire frame cube using OpenGL.

Pressing the "f" key toggles full screen game mode.
This full screen mode works with nVidia stereoscopic
driver for Windows XP.
"""
def __init__(self):
self.animation_interval = 100 # milliseconds
self.rotation_angle = 0.0 # degrees, starting point
glutInit("Cube.py")
glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH)
glEnable(GL_DEPTH_TEST)
glutInitWindowSize(200, 200)
# Remember window id for when we return from game mode.
self.window_id = glutCreateWindow('Wire Cube')
self.initialize_gl_context()
# glutTimerFunc remains when GL context is replaced,
# so it does not go into self.initialize_gl_context()
glutTimerFunc(self.animation_interval, self.animate, 1)
glutMainLoop() # never returns

def clear_gl_callbacks(self):
"""
Set inoccuous callbacks during times when no valid context may be available.
"""
glutDisplayFunc(do_nothing)
glutMotionFunc(None)
glutKeyboardFunc(None)

def initialize_gl_context(self):
"""
When switching between full-screen and windowed modes,
initialize_gl_context() reinitializes state.
"""
glClearColor(0.5,0.5,0.5,0.0)
glutDisplayFunc(self.display)
# glutPassiveMotionFunc(self.mouse_motion)
glutMotionFunc(self.mouse_motion)
glutKeyboardFunc(self.keypress)
# establish the projection matrix (perspective)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
x,y,width,height = glGetDoublev(GL_VIEWPORT)
gluPerspective(
45, # field of view in degrees
width/float(height or 1), # aspect ratio
.25, # near clipping plane
200, # far clipping plane
)

def start_game_mode(self):
if glutGameModeGet(GLUT_GAME_MODE_ACTIVE):
return # already in game mode
glutGameModeString("800x600:16@60")
if glutGameModeGet(GLUT_GAME_MODE_POSSIBLE):
self.clear_gl_callbacks()
glutEnterGameMode()
self.initialize_gl_context()

def start_windowed_mode(self):
if glutGameModeGet(GLUT_GAME_MODE_ACTIVE):
self.clear_gl_callbacks()
glutLeaveGameMode()
# Remember the window we created at start up?
glutSetWindow(self.window_id)
self.initialize_gl_context()

def display(self):
"""
"display()" method is called every time OpenGL updates the display.
"""
# Erase the old image
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
# Modelview must be set before geometry is sent
# or else crash when entering stereoscopic mode.
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
gluLookAt(
0,-0.5,5, # eyepoint
0,0,0, # center-of-view
0,1,0, # up-vector
)
# Rotate about the origin as animation progresses
glRotate(self.rotation_angle, 0, 1, 0)
glPushMatrix()
try:
# Draw the cube
glutWireCube(2.0)
finally:
glPopMatrix()
glutSwapBuffers()

def mouse_motion(self, x, y):
pass

def keypress(self, key, x, y):
if key == '\033':
# Escape key leaves full screen mode
if glutGameModeGet(GLUT_GAME_MODE_ACTIVE):
self.start_windowed_mode()
elif key == "f":
# "f" key toggle full screen and windowed mode.
if glutGameModeGet(GLUT_GAME_MODE_ACTIVE):
self.start_windowed_mode()
else:
self.start_game_mode()

def animate(self, value):
"""
Periodically change the rotation angle for the cube animation.

This animate method() is called as a glutTimerFunc().
"""
self.rotation_angle += 1.0
while self.rotation_angle > 360.0:
self.rotation_angle -= 360.0
glutPostRedisplay()
# Be sure to come back for more
glutTimerFunc(self.animation_interval, self.animate, value+1)


# Run the HelloOpenGL application when this script is run directly.
if (__name__ == '__main__'):
HelloOpenGL()