Friday, January 26, 2007

TableViewers and Nativelooking Checkboxes

Many of us have faced the same problem that if you needed to use checkboxes in your TableViewer/TreeViewer they look not native because they are pictures. I had some free minutes and thought that it's time to create a LabelProvider which is able to create platform look-and-feel images out of the box. It's not 100% native but it's not far away. The trick is to automatically create screenshots from CheckBox-Buttons and use them. This way the checkboxes look native on all platforms and correspond to the current look and feel of the platform.

Here's the code if someone is interested:


package at.bestsolution.jface.viewers;

import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.ColumnLabelProvider;
import org.eclipse.jface.viewers.ColumnViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Shell;

public abstract class EmulatedNativeCheckBoxLabelProvider
extends ColumnLabelProvider
{
private static final String CHECKED_KEY = "CHECKED";
private static final String UNCHECK_KEY = "UNCHECKED";

public EmulatedNativeCheckBoxLabelProvider(ColumnViewer viewer) {
if( JFaceResources.getImageRegistry().getDescriptor(CHECKED_KEY) == null ) {
JFaceResources.getImageRegistry().put(UNCHECK_KEY,
makeShot(viewer.getControl().getShell(),false));
JFaceResources.getImageRegistry().put(CHECKED_KEY,
makeShot(viewer.getControl().getShell(),true));
}
}

private Image makeShot(Shell shell, boolean type) {
Shell s = new Shell(shell,SWT.NO_TRIM);
Button b = new Button(s,SWT.CHECK);
b.setSelection(type);
Point bsize = b.computeSize(SWT.DEFAULT, SWT.DEFAULT);
b.setSize(bsize);
b.setLocation(0, 0);
s.setSize(bsize);
s.open();

GC gc = new GC(b);
Image image = new Image(shell.getDisplay(), bsize.x, bsize.y);
gc.copyArea(image, 0, 0);
gc.dispose();

s.close();

return image;
}

public Image getImage(Object element) {
if( isChecked(element) ) {
return JFaceResources.getImageRegistry().
getDescriptor(CHECKED_KEY).createImage();
} else {
return JFaceResources.getImageRegistry().
getDescriptor(UNCHECK_KEY).createImage();
}
}

protected abstract boolean isChecked(Object element);
}

I haven't really tested this (currently only on WinXP) but I suppose it's working on all platforms. You can also get the code from my svn-repository which holds some other interesting utilities and viewer classes.

15 comments:

André said...

neat stuff! thanks!
I catched a lot of your work, lately! :-)

Anonymous said...

That is a ludicrous hack. I LOVE IT!!!

Anonymous said...

However, you are able to add native (not native like) check boxes on the Tree/Table widget, which TreeViewer /TableViewer wraps.

Why can't the SWT.CHECK flag be pushed somehow into the underlying widget?

Anonymous said...

back with some more light on the mater. Here's a bit of code from the last JFace:

/**
* Creates a tree viewer on a newly-created tree control under the given
* parent. The tree control is created using the given SWT style bits. The
* viewer has no input, no content provider, a default label provider, no
* sorter, and no filters.
*
* @param parent
* the parent control
* @param style
* the SWT style bits used to create the tree.
*/
public TreeViewer(Composite parent, int style) {
this(new Tree(parent, style));
}

The style you specify here is passed unchanged to the inner Tree widget.

This worked perfectly for me:

TreeViewer viewer = new TreeViewer(viewerParent, SWT.CHECK);

Tom said...

I know SWT.CHECK but the problem is that with this you can only have a checkbox in the first column of your Table/Tree. This solution works in any column of a Viewer :-)

Anonymous said...

private Image makeShot(Control control, boolean type) {
Shell s = new Shell(control.getShell(), SWT.NO_TRIM);
// otherwise we have a default gray color
Color backgroundColor = control.getBackground();
s.setBackground(backgroundColor);
Button b = new Button(s, SWT.CHECK);
b.setBackground(backgroundColor);
b.setSelection(type);
// otherwise an image is located in a corner
b.setLocation(1, 1);
Point bsize = b.computeSize(SWT.DEFAULT, SWT.DEFAULT);
// otherwise an image is stretched by width
bsize.x=Math.max(bsize.x, bsize.y);
bsize.y=Math.max(bsize.x, bsize.y);
b.setSize(bsize);
s.setSize(bsize);
s.open();
GC gc = new GC(s);
Image image = new Image(control.getDisplay(), bsize.x, bsize.y);
gc.copyArea(image, 0, 0);
gc.dispose();
s.close();
return image;
}

Anonymous said...

here the fully tested and working code. improvements:

* checkbox is painted at the right position
* the correct beackground-color is used
* bugfix: the previous getImage() created a new image at every getImage()-call. this led to a NoMoreHandles-Exception


package de.fhmracing.glasseye.canexplorer.gui.transmit;

import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.ColumnLabelProvider;
import org.eclipse.jface.viewers.ColumnViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Shell;

public abstract class EmulatedNativeCheckBoxLabelProvider extends ColumnLabelProvider
{
private static final String CHECKED_KEY = "CHECKED";
private static final String UNCHECK_KEY = "UNCHECKED";

public EmulatedNativeCheckBoxLabelProvider(ColumnViewer viewer)
{
if (JFaceResources.getImageRegistry().getDescriptor(CHECKED_KEY) == null)
{
JFaceResources.getImageRegistry().put(UNCHECK_KEY, makeShot(viewer.getControl(), false));
JFaceResources.getImageRegistry().put(CHECKED_KEY, makeShot(viewer.getControl(), true));
}
}

private Image makeShot(Control control, boolean type)
{
Shell shell = new Shell(control.getShell(), SWT.NO_TRIM);

// otherwise we have a default gray color
Color backgroundColor = control.getBackground();
shell.setBackground(backgroundColor);

Button button = new Button(shell, SWT.CHECK);
button.setBackground(backgroundColor);
button.setSelection(type);

// otherwise an image is located in a corner
button.setLocation(1, 1);
Point bsize = button.computeSize(SWT.DEFAULT, SWT.DEFAULT);

// otherwise an image is stretched by width
bsize.x = Math.max(bsize.x-1, bsize.y-1);
bsize.y = Math.max(bsize.x-1, bsize.y-1);
button.setSize(bsize);
shell.setSize(bsize);

shell.open();
GC gc = new GC(shell);
Image image = new Image(control.getDisplay(), bsize.x, bsize.y);
gc.copyArea(image, 0, 0);
gc.dispose();
shell.close();

return image;
}

public Image getImage(Object element)
{
if (isChecked(element))
{
return JFaceResources.getImageRegistry().get(CHECKED_KEY);
}
else
{
return JFaceResources.getImageRegistry().get(UNCHECK_KEY);
}
}

protected abstract boolean isChecked(Object element);
}

Anonymous said...

here the fully tested and working code. improvements:

* checkbox is painted at the right position
* the correct beackground-color is used
* bugfix: the previous getImage() created a new image at every getImage()-call. this led to a NoMoreHandles-Exception


package de.fhmracing.glasseye.canexplorer.gui.transmit;

import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.ColumnLabelProvider;
import org.eclipse.jface.viewers.ColumnViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Shell;

public abstract class EmulatedNativeCheckBoxLabelProvider extends ColumnLabelProvider
{
private static final String CHECKED_KEY = "CHECKED";
private static final String UNCHECK_KEY = "UNCHECKED";

public EmulatedNativeCheckBoxLabelProvider(ColumnViewer viewer)
{
if (JFaceResources.getImageRegistry().getDescriptor(CHECKED_KEY) == null)
{
JFaceResources.getImageRegistry().put(UNCHECK_KEY, makeShot(viewer.getControl(), false));
JFaceResources.getImageRegistry().put(CHECKED_KEY, makeShot(viewer.getControl(), true));
}
}

private Image makeShot(Control control, boolean type)
{
Shell shell = new Shell(control.getShell(), SWT.NO_TRIM);

// otherwise we have a default gray color
Color backgroundColor = control.getBackground();
shell.setBackground(backgroundColor);

Button button = new Button(shell, SWT.CHECK);
button.setBackground(backgroundColor);
button.setSelection(type);

// otherwise an image is located in a corner
button.setLocation(1, 1);
Point bsize = button.computeSize(SWT.DEFAULT, SWT.DEFAULT);

// otherwise an image is stretched by width
bsize.x = Math.max(bsize.x-1, bsize.y-1);
bsize.y = Math.max(bsize.x-1, bsize.y-1);
button.setSize(bsize);
shell.setSize(bsize);

shell.open();
GC gc = new GC(shell);
Image image = new Image(control.getDisplay(), bsize.x, bsize.y);
gc.copyArea(image, 0, 0);
gc.dispose();
shell.close();

return image;
}

public Image getImage(Object element)
{
if (isChecked(element))
{
return JFaceResources.getImageRegistry().get(CHECKED_KEY);
}
else
{
return JFaceResources.getImageRegistry().get(UNCHECK_KEY);
}
}

protected abstract boolean isChecked(Object element);
}

Alexander Ljungberg said...

Nice hack! Unfortunately the checkbox gets a grey background on OS X instead of the blue or white table row background color.

Alexander Ljungberg said...

I came up with a a workaround that works for me on the Mac. I haven't tested it on Windows but I assume it'd work there too.

package de.fhmracing.glasseye.canexplorer.gui.transmit;

import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.ColumnLabelProvider;
import org.eclipse.jface.viewers.ColumnViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Shell;

public abstract class EmulatedNativeCheckBoxLabelProvider extends
ColumnLabelProvider {
private static final String CHECKED_KEY = "CHECKED";
private static final String UNCHECK_KEY = "UNCHECKED";

public EmulatedNativeCheckBoxLabelProvider(ColumnViewer viewer) {
if (JFaceResources.getImageRegistry().getDescriptor(CHECKED_KEY) == null) {
JFaceResources.getImageRegistry().put(UNCHECK_KEY,
makeShot(viewer.getControl(), false));
JFaceResources.getImageRegistry().put(CHECKED_KEY,
makeShot(viewer.getControl(), true));
}
}

private Image makeShot(Control control, boolean type)
{
// Hopefully no platform uses exactly this color because we'll make
// it transparent in the image.
Color greenScreen = new Color(control.getDisplay(), 222, 223, 224);

Shell shell = new Shell(control.getShell(), SWT.NO_TRIM);

// otherwise we have a default gray color
shell.setBackground(greenScreen);

Button button = new Button(shell, SWT.CHECK);
button.setBackground(greenScreen);
button.setSelection(type);

// otherwise an image is located in a corner
button.setLocation(1, 1);
Point bsize = button.computeSize(SWT.DEFAULT, SWT.DEFAULT);

// otherwise an image is stretched by width
bsize.x = Math.max(bsize.x - 1, bsize.y - 1);
bsize.y = Math.max(bsize.x - 1, bsize.y - 1);
button.setSize(bsize);
shell.setSize(bsize);

shell.open();
GC gc = new GC(shell);
Image image = new Image(control.getDisplay(), bsize.x, bsize.y);
gc.copyArea(image, 0, 0);
gc.dispose();
shell.close();

ImageData imageData = image.getImageData();
imageData.transparentPixel = imageData.palette.getPixel(greenScreen
.getRGB());

return new Image(control.getDisplay(), imageData);
}

public Image getImage(Object element) {
if (isChecked(element)) {
return JFaceResources.getImageRegistry().get(CHECKED_KEY);
} else {
return JFaceResources.getImageRegistry().get(UNCHECK_KEY);
}
}

protected abstract boolean isChecked(Object element);
}

Anonymous said...

one more thing i guess - if you set the background of the viewer a little border with that color will be visible when the respective row is selected. Setting

SWT.NO_BACKGROUND

flag on the Shell and Buttons eliminates the problem:

Shell shell = new Shell(control.getShell(), SWT.NO_TRIM | SWT.NO_BACKGROUND);


Button button = new Button(shell, SWT.CHECK | SWT.NO_BACKGROUND);

David Pérez said...

It works ok. :-)
Thanks for sharing this trick.

Tom Hofmann said...

I have published an updated version that uses the Control::print API available since SWT 3.4, and also supporting the grayed state of checkboxes.


See tkilla.ch/eclipse for the update source.

Tom said...

Would you like to make this part of our Snippet collection [*]http://wiki.eclipse.org/JFaceSnippets#Snippet061FakedNativeCellEditor.
Then please file a bugzilla and attach patch to it.

viagra pills said...

Excellent... very useful information. Thanks for sharing this code. I would like to see more unique update from you.

Regards
Alexa