From b222ad5dbc85956228b5dec5a2d4e009074c1f44 Mon Sep 17 00:00:00 2001 From: Heiko Klare Date: Fri, 14 Aug 2026 23:38:11 +0200 Subject: [PATCH] [Win32] Fix tab stop width in GDI+ text rendering The GDI+ based text rendering path in GC.drawText() derived the DRAW_TAB tab stop width from the width of a single space glyph, while the GDI based path and plain (non-advanced) GDI both use eight times the font's average character width, which is the convention Win32's own DrawText() and TabbedTextOut() follow. Those are two different metrics, not two ways of computing the same one: in proportional fonts a space is roughly half the average character width, so tab stops came out about half as wide whenever the GDI+ path was taken. In monospace fonts the two nearly coincide, which is why the discrepancy was easy to miss. The GDI+ path is changed to reuse the GDI text metric already computed by its caller, so that tab stops are sized consistently no matter which path renders the text. This restores the behaviour established for the GDI path in bug 289244 ("GDI+ drawText is not consistent with GDI"), which the GDI+ fallback path reintroduced in bug 305815 had missed. Add tests that pin down the tab stop width itself (eight average character widths), its consistency across both rendering paths, and two properties that must hold for either engine: consecutive tabs advance by whole tab stops, and text following a tab starts at the same tab stop regardless of what precedes the tab. They are measured from a leading tab, which expands to exactly one tab stop and so is not diluted by the slightly different glyph advances of the two engines, and cover both proportional and monospace fonts. Add Snippet395 as a manual/visual companion to the automated tests: it renders a series of text properties, one row per property, and offers a checkbox to switch between plain GDI and GDI+ (advanced) rendering at runtime, without restarting, to visually compare the resulting output. Assisted-by: Claude Opus 5 --- .../eclipse/swt/graphics/GCWin32Tests.java | 179 ++++++++++++++ .../win32/org/eclipse/swt/graphics/GC.java | 21 +- .../org.eclipse.swt.snippets/.classpath_cocoa | 2 +- .../org.eclipse.swt.snippets/.classpath_gtk | 2 +- examples/org.eclipse.swt.snippets/Snippets.md | 1 + .../previews/Snippet395.png | Bin 0 -> 38049 bytes .../org/eclipse/swt/snippets/Snippet395.java | 224 ++++++++++++++++++ 7 files changed, 417 insertions(+), 12 deletions(-) create mode 100644 examples/org.eclipse.swt.snippets/previews/Snippet395.png create mode 100644 examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet395.java diff --git a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/GCWin32Tests.java b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/GCWin32Tests.java index 47b20458042..d6af2d2c10b 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/GCWin32Tests.java +++ b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/GCWin32Tests.java @@ -18,6 +18,7 @@ import java.util.*; import java.util.concurrent.*; +import java.util.function.*; import java.util.stream.*; import org.eclipse.swt.*; @@ -146,6 +147,184 @@ private static int renderTextAndCountNonWhitePixels(Image target, Font font, Str return count; } + /** + * U+FFFE is a Unicode non-character that no standard font has a glyph for. + * Appending it to a string makes an advanced GC lay that string out with + * GDI+ instead of letting GDI compute the glyph positions, which is the only + * way to exercise the GDI+ tab stop handling. + */ + private static final String UNSUPPORTED_GLYPH = String.valueOf((char) 0xFFFE); + + /** + * Fonts covering both ends of the space-width/average-character-width ratio: + * in proportional fonts a space is roughly half the average character width, + * while in monospace fonts the two nearly coincide. A tab stop derived from + * the space width therefore only misbehaves noticeably for the proportional + * ones, so both kinds have to be covered. + */ + private static Stream tabStopTestFonts() { + return Stream.of("Segoe UI", "Arial", "Times New Roman", "Courier New", "Consolas"); + } + + /** + * The extents of the GDI and the GDI+ path are each rounded up to whole + * pixels independently, so measurements derived from a difference of two + * extents may legitimately be off by one pixel. + */ + private static final int ROUNDING_TOLERANCE = 1; + + /** + * Verifies that a tab is expanded to eight times the font's average + * character width, which is the convention Win32's own {@code DrawText()} + * and {@code TabbedTextOut()} follow: {@code TabbedTextOut()} is documented + * to expand tabs to "eight times the average character width" by default, + * and {@code DRAWTEXTPARAMS.iTabLength} is documented to be measured "in + * units equal to the average character width". + *

+ * This pins down the constant the GDI+ path is expected to reproduce. + */ + @ParameterizedTest + @MethodSource("tabStopTestFonts") + public void tabStopWidthEqualsEightAverageCharacterWidths(String fontName) { + Display display = Display.getDefault(); + Image image = new Image(display, 400, 100); + Font font = new Font(display, fontName, 12, SWT.NORMAL); + GC gc = new GC(image); + try { + gc.setFont(font); + int averageCharacterWidth = gc.getFontMetrics().handle.tmAveCharWidth; + assertWithinRoundingTolerance(8 * averageCharacterWidth, measureTabStopWidth(gc), + "a tab must be expanded to eight average character widths for font " + fontName); + } finally { + gc.dispose(); + font.dispose(); + image.dispose(); + } + } + + /** + * Verifies that tab stops are expanded to the same width no matter whether + * text is rendered via plain GDI or via GDI+. + *

+ * Both paths must use eight times the font's average character width + * ({@code TEXTMETRIC.tmAveCharWidth}). The GDI+ path used to derive its tab + * stop width from the width of a single space glyph instead. That is a + * different metric, not merely a differently computed one: in proportional + * fonts a space is roughly half the average character width, so tab stops + * came out about half as wide whenever that path was taken. + */ + @ParameterizedTest + @MethodSource("tabStopTestFonts") + public void tabStopWidthIsConsistentBetweenGdiAndGdipRendering(String fontName) { + Display display = Display.getDefault(); + Image image = new Image(display, 400, 100); + Font font = new Font(display, fontName, 12, SWT.NORMAL); + try { + int gdiTabStopWidth = withGC(image, font, false, GCWin32Tests::measureTabStopWidth); + int gdipTabStopWidth = withGC(image, font, true, GCWin32Tests::measureTabStopWidth); + assertWithinRoundingTolerance(gdiTabStopWidth, gdipTabStopWidth, + "GDI+ rendering must expand a tab to the same width as GDI rendering for font " + fontName); + } finally { + font.dispose(); + image.dispose(); + } + } + + /** + * Verifies that consecutive tabs advance to consecutive tab stops instead of + * collapsing into a single one, i.e. that the tab stop width repeats. GDI + * derives the repetition from its own {@code (position / width + 1) * width} + * calculation, whereas GDI+ gets a single tab stop distance passed to + * {@code StringFormat::SetTabStops} and repeats it internally; this asserts + * that both arrive at the same layout. + */ + @ParameterizedTest + @MethodSource("tabStopTestFonts") + public void consecutiveTabsAdvanceByWholeTabStops(String fontName) { + Display display = Display.getDefault(); + Image image = new Image(display, 800, 100); + Font font = new Font(display, fontName, 12, SWT.NORMAL); + try { + for (boolean advanced : new boolean[] { false, true }) { + int twoTabStops = withGC(image, font, advanced, + gc -> measureTabAdvance(gc, "\t\t") ); + int oneTabStop = withGC(image, font, advanced, GCWin32Tests::measureTabStopWidth); + assertWithinRoundingTolerance(2 * oneTabStop, twoTabStops, + "two tabs must advance by two tab stops for font " + fontName + + " (advanced=" + advanced + ")"); + } + } finally { + font.dispose(); + image.dispose(); + } + } + + /** + * Verifies that a tab advances to the next tab stop rather than adding a + * fixed amount of space, so that text following a tab starts at the same + * column regardless of what precedes the tab within the same tab stop. + */ + @ParameterizedTest + @MethodSource("tabStopTestFonts") + public void textAfterTabStartsAtSameTabStopRegardlessOfPrecedingText(String fontName) { + Display display = Display.getDefault(); + Image image = new Image(display, 400, 100); + Font font = new Font(display, fontName, 12, SWT.NORMAL); + try { + for (boolean advanced : new boolean[] { false, true }) { + int withoutPrefix = withGC(image, font, advanced, gc -> measureExtent(gc, "\tB")); + int withPrefix = withGC(image, font, advanced, gc -> measureExtent(gc, "A\tB")); + assertWithinRoundingTolerance(withoutPrefix, withPrefix, + "text following a tab must start at the same tab stop no matter what precedes the tab, " + + "for font " + fontName + " (advanced=" + advanced + ")"); + } + } finally { + font.dispose(); + image.dispose(); + } + } + + /** + * Returns the width of a single tab stop, measured as the advance a leading + * tab adds. A leading tab always expands to exactly one tab stop, so unlike + * a tab in the middle of a string this measurement is not diluted by the + * slightly different glyph advances of the GDI and the GDI+ text layout + * engine. + */ + private static int measureTabStopWidth(GC gc) { + return measureTabAdvance(gc, "\t"); + } + + /** + * Returns the horizontal advance the given leading tabs add to the extent of + * the text that follows them. + */ + private static int measureTabAdvance(GC gc, String leadingTabs) { + return measureExtent(gc, leadingTabs + "B") - measureExtent(gc, "B"); + } + + private static int measureExtent(GC gc, String text) { + // measure in pixels to keep the comparison free of the point/pixel + // conversion the public API applies at non-100% zoom levels + return gc.textExtentInPixels(text + UNSUPPORTED_GLYPH, SWT.DRAW_TAB).x; + } + + private static void assertWithinRoundingTolerance(int expected, int actual, String message) { + assertTrue(Math.abs(actual - expected) <= ROUNDING_TOLERANCE, + message + " (expected " + expected + ", was " + actual + ")"); + } + + private static int withGC(Image target, Font font, boolean advanced, ToIntFunction measurement) { + GC gc = new GC(target); + try { + gc.setFont(font); + gc.setAdvanced(advanced); + return measurement.applyAsInt(gc); + } finally { + gc.dispose(); + } + } + /** * Regression test for the size calculation in scaling/cropping GC.drawImage() * operations with asymmetric source dimensions (smaller height than width) at diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java index 525cf88296d..cb4246c32d8 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java @@ -2886,7 +2886,7 @@ void drawText(long gdipGraphics, String string, int x, int y, int flags, Point s if (hFont != 0) OS.SelectObject(hdc, oldFont); Gdip.Graphics_ReleaseHDC(gdipGraphics, hdc); if (gdip) { - drawTextGDIP(gdipGraphics, string, x, y, flags, size == null, size); + drawTextGDIP(gdipGraphics, string, x, y, flags, size == null, size, lptm); return; } int i = 0, start = 0, end = 0, drawX = x, drawY = y, width = 0, mnemonicIndex = -1; @@ -3056,7 +3056,7 @@ private RectF drawText(long gdipGraphics, char[] buffer, int start, int length, return bounds; } -private void drawTextGDIP(long gdipGraphics, String string, int x, int y, int flags, boolean draw, Point size) { +private void drawTextGDIP(long gdipGraphics, String string, int x, int y, int flags, boolean draw, Point size, TEXTMETRIC lptm) { boolean needsBounds = !draw || (flags & SWT.DRAW_TRANSPARENT) == 0; char[] buffer; if ((flags & SWT.DRAW_DELIMITER) == 0) { @@ -3078,7 +3078,15 @@ private void drawTextGDIP(long gdipGraphics, String string, int x, int y, int fl int formatFlags = Gdip.StringFormat_GetFormatFlags(format) | Gdip.StringFormatFlagsMeasureTrailingSpaces; if ((data.style & SWT.MIRRORED) != 0) formatFlags |= Gdip.StringFormatFlagsDirectionRightToLeft; Gdip.StringFormat_SetFormatFlags(format, formatFlags); - float[] tabs = (flags & SWT.DRAW_TAB) != 0 ? new float[]{measureSpace(data.gdipFont, format) * 8} : new float[1]; + // Use the same tab stop width as the GDI-based text rendering path: 8 * the + // font's average character width, which is what Win32's own DrawText() and + // TabbedTextOut() use by default. This used to be 8 * the width of a single + // space glyph, which is a different metric rather than a differently + // computed one: in proportional fonts a space is roughly half the average + // character width, so tab stops came out about half as wide whenever this + // path was taken. (In monospace fonts the two nearly coincide, which is why + // the discrepancy was easy to miss.) + float[] tabs = (flags & SWT.DRAW_TAB) != 0 ? new float[]{lptm.tmAveCharWidth * 8} : new float[1]; Gdip.StringFormat_SetTabStops(format, 0, tabs.length, tabs); int hotkeyPrefix = (flags & SWT.DRAW_MNEMONIC) != 0 ? Gdip.HotkeyPrefixShow : Gdip.HotkeyPrefixNone; if ((flags & SWT.DRAW_MNEMONIC) != 0 && (data.uiState & OS.UISF_HIDEACCEL) != 0) hotkeyPrefix = Gdip.HotkeyPrefixHide; @@ -4658,13 +4666,6 @@ public boolean isDisposed() { return handle == 0; } -private float measureSpace(long font, long format) { - PointF pt = new PointF(); - RectF bounds = new RectF(); - Gdip.Graphics_MeasureString(data.gdipGraphics, new char[]{' '}, 1, font, pt, format, bounds); - return bounds.Width; -} - /** * Sets the receiver to always use the operating system's advanced graphics * subsystem for all graphics operations if the argument is true. diff --git a/examples/org.eclipse.swt.snippets/.classpath_cocoa b/examples/org.eclipse.swt.snippets/.classpath_cocoa index bcad73af751..6f6d5d56841 100644 --- a/examples/org.eclipse.swt.snippets/.classpath_cocoa +++ b/examples/org.eclipse.swt.snippets/.classpath_cocoa @@ -2,6 +2,6 @@ - + diff --git a/examples/org.eclipse.swt.snippets/.classpath_gtk b/examples/org.eclipse.swt.snippets/.classpath_gtk index bcad73af751..6f6d5d56841 100644 --- a/examples/org.eclipse.swt.snippets/.classpath_gtk +++ b/examples/org.eclipse.swt.snippets/.classpath_gtk @@ -2,6 +2,6 @@ - + diff --git a/examples/org.eclipse.swt.snippets/Snippets.md b/examples/org.eclipse.swt.snippets/Snippets.md index 08cd32f387c..9f3e1becfdd 100644 --- a/examples/org.eclipse.swt.snippets/Snippets.md +++ b/examples/org.eclipse.swt.snippets/Snippets.md @@ -198,6 +198,7 @@ To contribute a new snippet, [create a snippet contribution as a pull request](h - [draw 2 polylines with different line attributes](https://github.com/eclipse-platform/eclipse.platform.swt/tree/master/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet252.java) – [(preview)](https://github.com/eclipse-platform/eclipse.platform.swt/blob/master/examples/org.eclipse.swt.snippets/previews/Snippet252.png "Preview for Snippet 252") - [draw lines with configurable line width, scaling and rotation](https://github.com/eclipse-platform/eclipse.platform.swt/tree/master/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet381.java) – [(preview)](https://github.com/eclipse-platform/eclipse.platform.swt/blob/master/examples/org.eclipse.swt.snippets/previews/Snippet381.png "Preview for Snippet 381") - [crop and scale images via source and destination values](https://github.com/eclipse-platform/eclipse.platform.swt/tree/master/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet389.java) – [(preview)](https://github.com/eclipse-platform/eclipse.platform.swt/blob/master/examples/org.eclipse.swt.snippets/previews/Snippet389.png "Preview for Snippet 389") +- [render different types of text with GDI vs. GDI+ (Windows-only)](https://github.com/eclipse-platform/eclipse.platform.swt/tree/master/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet395.java) – [(preview)](https://github.com/eclipse-platform/eclipse.platform.swt/blob/master/examples/org.eclipse.swt.snippets/previews/Snippet395.png "Preview for Snippet 395") ### **Gesture, Touch support** - [create a shell and listen for TouchEvents](https://github.com/eclipse-platform/eclipse.platform.swt/tree/master/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet352.java) diff --git a/examples/org.eclipse.swt.snippets/previews/Snippet395.png b/examples/org.eclipse.swt.snippets/previews/Snippet395.png new file mode 100644 index 0000000000000000000000000000000000000000..b802778b031b7cc1f2ec63d3a34ec2a616d36979 GIT binary patch literal 38049 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 knwnAA`p renderSamples(Display display, java.util.List rows, + Map fonts, int sampleWidth, int sampleHeight, boolean advanced) { + Map samples = new HashMap<>(); + for (TextRow row : rows) { + Image sample = new Image(display, sampleWidth, sampleHeight); + GC gc = new GC(sample, row.gcStyle()); + try { + gc.setAdvanced(advanced); + gc.setBackground(new Color(255, 255, 255)); + gc.fillRectangle(sample.getBounds()); + gc.setForeground(new Color(0, 0, 0)); + gc.setFont(fonts.get(row)); + gc.drawText(row.text(), 5, 5, row.drawFlags()); + } finally { + gc.dispose(); + } + samples.put(row, sample); + } + return samples; + } + + @SuppressWarnings("restriction") + public static void main(String[] args) { + // U+FFFE has no glyph in any standard font. Appending it to a string makes + // GC.drawText() lay out that whole string with GDI+ instead of computing + // the glyph positions with GDI, so the rows using it always show what + // GDI+'s own text layout does with the respective text property. + String unsupportedGlyph = String.valueOf((char) 0xFFFE); + + java.util.List rows = new ArrayList<>(); + rows.add(new TextRow("Plain text", "Hello World")); + rows.add(new TextRow("Bold", SWT.BOLD, false, false, "Hello World", SWT.DRAW_TRANSPARENT, SWT.NONE)); + rows.add(new TextRow("Italic", SWT.ITALIC, false, false, "Hello World", SWT.DRAW_TRANSPARENT, SWT.NONE)); + rows.add(new TextRow("Underlined", SWT.NORMAL, true, false, "Hello World", SWT.DRAW_TRANSPARENT, SWT.NONE)); + rows.add(new TextRow("Strikeout", SWT.NORMAL, false, true, "Hello World", SWT.DRAW_TRANSPARENT, SWT.NONE)); + rows.add(new TextRow("Bold + underlined", SWT.BOLD, true, false, "Hello World", SWT.DRAW_TRANSPARENT, + SWT.NONE)); + rows.add(new TextRow("Underlined + unsupported glyph (U+FFFE)", SWT.NORMAL, true, false, + "Hi" + unsupportedGlyph, SWT.DRAW_TRANSPARENT, SWT.NONE)); + rows.add(new TextRow("Mnemonic (accelerator underline)", SWT.NORMAL, false, false, "&File", + SWT.DRAW_MNEMONIC | SWT.DRAW_TRANSPARENT, SWT.NONE)); + rows.add(new TextRow("Tab-separated columns", SWT.NORMAL, false, false, "A\tB\tC", SWT.DRAW_TAB, + SWT.NONE)); + rows.add(new TextRow("Tab-separated columns + unsupported glyph (U+FFFE)", SWT.NORMAL, false, false, + "A\tB\tC" + unsupportedGlyph, SWT.DRAW_TAB, SWT.NONE)); + rows.add(new TextRow("Kerning pair", "AVATAR WAVE To Yes")); + rows.add(new TextRow("Mirrored / RTL", SWT.NORMAL, false, false, "Hello World", SWT.DRAW_TRANSPARENT, + SWT.RIGHT_TO_LEFT)); + rows.add(new TextRow("Arabic", "\u0645\u0631\u062d\u0628\u0627 \u0628\u0627\u0644\u0639\u0627\u0644\u0645")); + rows.add(new TextRow("Hebrew", "\u05e9\u05dc\u05d5\u05dd \u05e2\u05d5\u05dc\u05dd")); + rows.add(new TextRow("CJK (Chinese)", "\u4f60\u597d\u4e16\u754c")); + rows.add(new TextRow("Cyrillic", "\u041f\u0440\u0438\u0432\u0435\u0442 \u043c\u0438\u0440")); + rows.add(new TextRow("Greek", "\u0393\u03b5\u03b9\u03ac \u03c3\u03bf\u03c5 \u039a\u03cc\u03c3\u03bc\u03b5")); + rows.add(new TextRow("Combining diacritics", "e\u0301clat na\u0308\u0131ve")); + + Display display = new Display(); + Shell shell = new Shell(display); + shell.setLayout(new GridLayout()); + shell.setText("Text rendering comparison"); + + Label info = new Label(shell, SWT.WRAP); + info.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + Button advancedCheckbox = new Button(shell, SWT.CHECK | SWT.WRAP); + advancedCheckbox.setText("Use GDI+ (advanced) rendering - GC.setAdvanced(true); " + + "uncheck to compare against plain GDI (GC.setAdvanced(false))"); + advancedCheckbox.setSelection(true); + advancedCheckbox.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + ScrolledComposite scroller = new ScrolledComposite(shell, SWT.V_SCROLL | SWT.BORDER); + scroller.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + scroller.setExpandHorizontal(true); + scroller.setExpandVertical(true); + + int rowHeight = 42; + int labelWidth = 260; + int sampleWidth = 320; + int sampleHeight = rowHeight - 4; + + Canvas canvas = new Canvas(scroller, SWT.NONE); + canvas.setSize(labelWidth + sampleWidth + 20, rows.size() * rowHeight + 10); + scroller.setContent(canvas); + scroller.setMinSize(canvas.getSize()); + + Font systemFont = display.getSystemFont(); + Map fonts = new HashMap<>(); + for (TextRow row : rows) { + FontData fontData = systemFont.getFontData()[0]; + fontData.setStyle(row.fontStyle()); + if (row.underline()) fontData.data.lfUnderline = 1; + if (row.strikeout()) fontData.data.lfStrikeOut = 1; + fonts.put(row, new Font(display, fontData)); + } + + // Mutable holder so the checkbox listeners can swap in a freshly rendered + // set of samples (and dispose the previous ones) whenever a toggle changes. + // Starts out empty; the initial samples are rendered by the refresh below. + AtomicReference> samplesHolder = new AtomicReference<>(Map.of()); + + Runnable refresh = () -> { + boolean advanced = advancedCheckbox.getSelection(); + + Map old = samplesHolder.getAndSet( + renderSamples(display, rows, fonts, sampleWidth, sampleHeight, advanced)); + old.values().forEach(Image::dispose); + + shell.setText("Text rendering comparison (advanced = " + advanced + ")"); + info.setText("GC.setAdvanced(" + advanced + ")" + + "\nToggle the checkbox below to compare plain GDI vs. GDI+ text rendering." + + " See the source comment for what to expect per row."); + shell.layout(true, true); + canvas.redraw(); + }; + + advancedCheckbox.addListener(SWT.Selection, e -> refresh.run()); + refresh.run(); + + canvas.addPaintListener(e -> { + GC gc = e.gc; + int y = 5; + for (TextRow row : rows) { + gc.drawText(row.label(), 5, y + (rowHeight - 4) / 4, SWT.DRAW_TRANSPARENT); + gc.drawImage(samplesHolder.get().get(row), labelWidth, y); + y += rowHeight; + } + }); + + canvas.addDisposeListener(e -> { + samplesHolder.get().values().forEach(Image::dispose); + fonts.values().forEach(Font::dispose); + }); + + shell.setSize(700, 500); + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + display.dispose(); + } +}