Embedding HIViews in Qt/Mac Applications

Материал из Wiki.crossplatform.ru

Перейти к: навигация, поиск
Image:qt-logo_new.png Image:qq-title-article.png
Qt Quarterly | Выпуск 20 | Документация

by Trenton Schulz

Unlike earlier Qt versions, Qt 4 for Mac OS X is built on the top ofthe HIToolbox APIs which form Apple's modern Carbon interface. Thismeans that Qt can interact with other HIToolBox APIs for extrafunctionality. We'll take a look at this by showing how to embedHIViews inside a Qt application.

Содержание

When Qt was originally introduced on Mac OS X, a Qt window consisted of awindow with regions for various areas of the widgets. While this allowed us todo quite a bit with our widgets, it made it very difficult to integrate Qtwidgets with other applications or vice versa. Typically, one worked around thisby using separate windows or by possessing intimate knowledge of how Qt dealtwith widgets, but this was hardly optimal.

We wanted to remove this limitation in Qt 4, so we jumped onto the HIViewbandwagon. This was Apple's new way of creating their own widgets in the Carbonworld. This meant that every QWidget was now a HIView and could be manipulatedand used just like other HIViews. This also gave us the benefit of compositingwithout any extra work on our part.

Still, it was not all perfect. You could now embed Qt widgets in other non-Qtwindows and vice versa, but it wasn't straightforward. It still requiredknowledge about how Qt deals with events and some custom code to set it up.It was certainly possible, but more work than you would typicallyexpect when using Qt.

For Qt 4.2, dropping support for Mac OS X 10.2 suddenly allowed us toput QWidgets inside menus. Since we were working on putting Qtwidgets in non-Qt windows, we decided to go a bit further and make iteasy for anyone to add a HIView into a QWidget.

With Qt 4.2, it is easier than ever to embedother Carbon HIViews (hereafter referred to as just "views") in Qt.

center

[править] Getting Started

In general, adding a standard view to a Qt widget hierarchy basically involvesthe following steps:

  • Wrap the view into a QWidget subclass.
  • In the constructor, create your view and pass the HIViewRef to theprotected QWidget::create() function.
  • Wrap any additional functions that map into Qt concepts.
  • Wrap any other view-specific functionality depending on how much you wantit to interact with other widgets.

Steps 3 and 4 are optional. With a little effort, you can make a custom viewact very much like a Qt widget, complete with properties, signals and slots.If all you need to do is "just get the thing in there", you can stop atstep 2.

[править] Using HIMovieView

It makes most sense to embed something that is a bit different to a standardQt widget. So, let's take a HIMovieView and add it to a Qt window.

HIMovieView is a new view in Mac OS X 10.4 (Tiger) or QuickTime 7 and it letsyou play QuickTime content. In previous versions of Mac OS X, there was the older"Movie Control", but HIMovieView adds simplicity and performance at the priceof only being available on systems with QuickTime 7. Unfortunately, it willonly be a 32-bit option on future versions of Mac OS X.

Regardless, it's still a great view for illustrating how to embed views in Qtwithout complicating the example unnecessarily. Here's the class declaration:

class MovieWidget : public QWidget
{
    Q_OBJECT
    Q_PROPERTY(bool nativeControlsVisible
               READ nativeControlsVisible
               WRITE setNativeControlsVisible)
 
public:
    MovieWidget(QWidget *parent = 0);
    ~MovieWidget();
 
    bool nativeControlsVisible() const;
    void setNativeControlsVisible(bool visible);
 
    QSize sizeHint() const;
 
signals:
    void optimalSizeChanged();
 
public slots:
    void loadMovie(const QString &fileName);
 
private:
    Movie currentMovie;
};

Our movie widget isn't too different than most widgets. We have a propertyindicating whether or not we want to have the native QuickTime controls. Weimplement sizeHint() because movies generally have a certain size they want tobe shown in. We also provide a signal so that a window can catch any suchchange, and we've provided a loadMovie() slot.

MovieWidget::MovieWidget(QWidget *parent)
    : QWidget(parent),
      currentMovie(0)
{
    HIViewRef movieView;
 
    HIObjectCreate(kHIMovieViewClassID, 0,
            reinterpret_cast<HIObjectRef *>(&amp;movieView));
 
    HIMovieViewChangeAttributes(movieView,
            kHIMovieViewAutoIdlingAttribute
            | kHIMovieViewControllerVisibleAttribute, 0);
 
    create(WId(movieView));
 
    setSizePolicy(QSizePolicy::MinimumExpanding,
                  QSizePolicy::MinimumExpanding);
}

Our constructor creates an instance of HIMovieView and sets itsattributes. You may wonder why we do this with the rather genericHIObjectCreate() and not the documented HIMovieViewCreate()function. This is to work around a bug in QuickTime 7.0. For laterversions of QuickTime, we could use HIMovieViewCreate().

After we have done that, we call the QWidget::create()function, which will set the HIViewRef as this QWidget's window ID.From here, we can always get back the HIViewRef by calling winId() andcasting it.

Since movies have a minimum size and look fine when expanded, weadjust our size policy accordingly.

MovieWidget::~MovieWidget()
{
  if (currentMovie != 0)
        DisposeMovie(currentMovie);
}

Our destructor cleans up the current movie &emdash; leaking movies can be expensive.

bool MovieWidget::nativeControlsVisible() const
{
    OptionBits currentBits = HIMovieViewGetAttributes(
        HIViewRef(winId()));
    return kHIMovieViewControllerVisibleAttribute &amp;
           currentBits;
}
 
void MovieWidget::setNativeControlsVisible(bool visible)
{
    if (visible != nativeControlsVisible()) {
        HIMovieViewChangeAttributes(HIViewRef(winId()),
          visible ?
            kHIMovieViewControllerVisibleAttribute : 0,
          !visible ?
            kHIMovieViewControllerVisibleAttribute : 0);
        updateGeometry();
        emit optimalSizeChanged();
    }
}

Our native controls property simply passes values through to theHIMovieView API, insulating us from the more clumsy "change attributes"pattern.

Because changing the visibility of the control changes its size, we need toupdate our geometry and emit a signal to indicate that it has changed, soothers can take this into account.

The loadMovie() slot may look complex, but it is primarily set up forQuickTime. Since QuickTime has its own documentation, we'll gloss over most ofthe details.

void MovieWidget::loadMovie(const QString &amp;fileName)
{
    QTVisualContextRef visualContext = 0;
    Boolean active = true;
    DataReferenceRecord dataRef;
    Movie oldMovie = 0;
 
    QTNewMoviePropertyElement newMovieProperties[] = {
        { kQTPropertyClass_DataLocation,
          kQTDataLocationPropertyID_DataReference,
          sizeof(dataRef), &amp;dataRef, 0 },
        { kQTPropertyClass_NewMovieProperty,
          kQTNewMoviePropertyID_Active,
          sizeof(active), &amp;active, 0 },
        { kQTPropertyClass_Context,
          kQTContextPropertyID_VisualContext,
          sizeof(visualContext), &amp;visualContext, 0 }
    };
 
    CFStringRef cfString = CFStringCreateWithCharacters(0,
            reinterpret_cast<const UniChar *>(
                fileName.unicode()), fileName.length());
    QTNewDataReferenceFromFullPathCFString(cfString,
            kQTPOSIXPathStyle, 0, &amp;dataRef.dataRef,
            &amp;dataRef.dataRefType);
    CFRelease(cfString);

The key point is that we first need to create a QuickTimereference to the file. We do this by converting our QString to a CoreFoundation string (CFStringRef). Since each CFStringRef isreference-counted, we need to remove our reference to it when we are done withit by calling CFRelease().

We replace the old Movie with a newly-created one and perform all thenecessary housekeeping to keep QuickTime happy, then we use theHIMovieViewSetMovie() function to embed the Movie into ourwidget:

    oldMovie = currentMovie;
    currentMovie = 0;
 
    NewMovieFromProperties(sizeof(newMovieProperties) /
        sizeof(newMovieProperties[0]),
        newMovieProperties, 0, 0, &amp;currentMovie);
    DisposeHandle(dataRef.dataRef);
    HIMovieViewSetMovie(HIViewRef(winId()), currentMovie);
    if (oldMovie != 0)
        DisposeMovie(oldMovie);
 
    updateGeometry();
    emit optimalSizeChanged();
}

Before we return, we call updateGeometry(), emitting our customoptimalSizeChanged() signal to indicate that the size has changed.

QSize MovieWidget::sizeHint() const
{
    EventRef event;
    HIRect optimalBounds;
    CreateEvent(0, kEventClassControl,
        kEventControlGetOptimalBounds,
        GetCurrentEventTime(),
        kEventAttributeUserEvent, &amp;event);
 
    SendEventToEventTargetWithOptions(event,
        HIObjectGetEventTarget(HIObjectRef(winId())),
        kEventTargetDontPropagate);
 
    GetEventParameter(event,
        kEventParamControlOptimalBounds, typeHIRect,
        0, sizeof(HIRect), 0, &amp;optimalBounds);
 
    ReleaseEvent(event);
    return QSize(optimalBounds.size.width,
                 optimalBounds.size.height);
}

The HIMovieView implements a Carbon event calledkEventControlGetOptimalBounds that we can use for calculating thesizeHint().Since Carbon does not have such things as virtual functions, we follow theCarbon pattern of sending the event and getting the information back which isreturned in an HIRect. We can then return the new optimal size.Now that we've created our HIMovieView, we can put it into a widgethierarchy, make QObject connections and do otherthings that you would expect with any other widget. As illustrated here:

Window::Window(QWidget *parent)
    : QWidget(parent)
{   
    setupUi(this);
 
    movieWidget = new MovieWidget(this);
    connect(this, SIGNAL(movieChosen(QString)),
            movieWidget, SLOT(loadMovie(QString)));
    connect(movieWidget, SIGNAL(optimalSizeChanged()),
            this, SLOT(updateSizes()));
    layout()->addWidget(movieWidget);
    ...
}

Since we are using QuickTime, we need to initialize the QuickTime environment.The typical place to do this is in the main() function:

int main(int argc, char **argv)
{
    QApplication app(argc, argv);
    EnterMovies();              // initialize QuickTime
    Window movieWindow;
    movieWindow.show();
    QMetaObject::invokeMethod(&amp;movieWindow, "chooseMovie",
                              Qt::QueuedConnection);
    int ret = app.exec();
    ExitMovies();               // uninitialize QuickTime
    return ret;
}

The call to EnterMovies() initializes the QuickTime environment, whileExitMovies() does the necessary tear down.

Now we can watch movies like, for example, the "Qt 4 Dance" inside Qt.

[править] Using HIWebView

Another popular view that developers may want to use is HIWebView. Ofcourse, HIWebView could be considered "androgynous" since it is actuallya Cocoa WebView that is then wrapped inside a Carbon API. This isthe only Cocoa view that is available in Carbon and it entails a number ofissues. One of them is that it can't really be composited with other widgetsin the widget hierarchy (i.e., you can't put other views on top of it).

Also, the Cocoa view haswhat would be referred to in X11 as "southwest gravity" which means that itexpands "up and to the right". This is all well and good, but the Carbon viewthat wraps it has "northwest gravity" which means that you will have somestrange resizing behavior if you don't handle the laying out of the views.Using QLayout helps shield you from this problem.

Another thing to beaware of is that HIWebView functions just create the view, the majority ofthe heavy lifting must be done through the Cocoa class. So, depending on howadvanced you want to be, you may need to take a look at Objective-C/Cocoa.

Despite these issues, creating and embedding an HIWebView is not that muchdifferent than the HIMovieView above. We'll take a look at creating thewidget and setting a URL on it:

WebWidget::WebWidget(QWidget *parent)
    : QWidget(parent)
{
    HIViewRef webView;
    WebInitForCarbon();
    HIWebViewCreate(&amp;webView);
    create(WId(webView));
}

This is similar to what we had to do with HIMovieView. We first call ourWebInitForCarbon(), a WebKit function that initializes the various bitsfor using WebKit. We then call HIWebViewCreate() to create ourHIWebView, we then pass it along to the QWidget::create() function.At that point we have successfully created an HIWebView.

The setUrl() function mixes the fun of C, C++, and Objective-C into onefunction! Setting a URL on the HIWebView requires us to convert our QUrl to a NSURL and tell the web view to load it.

void WebWidget::setUrl(const QUrl &amp;url)
{   
  if (!url.isValid())
      return;
 
    QString qurlString = url.toString();
    CFStringRef urlStr = CFStringCreateWithCharacters(0,
        reinterpret_cast<const UniChar *>(
            qurlStr.unicode()), qurlStr.size());
    CFURLRef urlRef = CFURLCreateWithString(0, urlStr, 0);
 
    WebView *cocoaView = HIWebViewGetWebView(
                         HIViewRef(winId()));
    NSURLRequest *request = [
        NSURLRequest requestWithURL:(NSURL *)urlRef];
    [[cocoaView mainFrame] loadRequest:request];
 
    CFRelease(urlStr);
    CFRelease(urlRef);
}

We accomplish our conversion by making a CFStringRef from the QString representationof the QUrl. We create a CFURLRef from this CFStringRef.We then use Apple's "toll-free bridging" concept (which boils down to thefact that the CFURLRef is interchangeable with a NSURL pointer) tocreate a NSURLRequest which we can then feed to the web view's renderingframe. We remove our references at the end of the function to keep thingsfrom leaking.Since we are mixing both Objective-C and C++ in the same source file, we savethe file with a .mm extension to indicate that it is an Objective-C++ file. Wealso put it in the OBJECTIVE_SOURCES area of our .pro file (instead ofthe regular SOURCES section), as shown below.

HEADERS += webwidget.h window.h
FORMS += window.ui
SOURCES += main.cpp window.cpp
OBJECTIVE_SOURCES += webwidget.mm
 
LIBS += -framework WebKit

Finally, here's a screenshot of the above WebWidget.

center

As mentioned above, this is really just scratching the surface forHIWebView; if you want full functionality (like knowing when a web pagehas been downloaded, updating the address bar when a link is clicked, etc.),you need to work with delegates, listeners, and other Cocoa/Objective-Cpatterns. We'll leave that to other documentation projects.

[править] Conclusion

With Qt 4.2, the process of embedding HIViews is fairlystraightforward. We've seen that the basics of creating them are moreor less the same for two of the more complex views out there.Hopefully, being able to embed HIViews will make development with Qt/Maca bit more of a cosmopolitan experience.

The full source code for the examples in this article can be downloadedfrom the Qt Quarterly website.



Copyright © 2007 Trolltech Trademarks