Chrome学习小记4:基于Chrome Views框架创建最小示例窗口B(动手写代码)

前言

​ 上一篇博客中,我们理解了一下example_main做的事情,现在我们就要进一步自己动手写代码了!那我们如何创建窗口呢?我们点击example里面具体做的事情,我们发现,是一个叫做views::Widget的类完成了我们的创口的本体工作。想要让我们的窗口出现自己的绘制样式,就需要一个代理绘制来完成我们的工作。这里笔者就略去了思考的流程(比如说我是怎么爬example找到关键点的,没啥技术含量更没意思,纯粹依靠Clangd的跳转解析),我们单刀直入话题。

​ 我们的工作核心就是views::Views,views::Delegate和views::Widget三个基本的类。我们会依次的来看看这几个类的工作情况,来决定我们的参数如何抉择

views::Widget 绘制层的Window

​ 我们打开ui\views\widget\widget.h文件仔细观察一下,开幕我们就能看到我们关心的views::Widget。谷歌的开发者指出了这些要点:

Widget 是一个平台独立的类型,它与特定于平台或上下文的 NativeWidget 实现进行通信。拥有一个 RootView,从而拥有一个视图层级结构。可以包含子 Widget。必须注意的是——所有 Widget 都应使用 ownership = CLIENT_OWNS_WIDGET。创建 Widget 的客户端代码应该持有 std::unique_ptr<Widget>。关闭 Widget 的正确方法是重置(reset)该 unique_ptrClose()CloseWithReason() 方法存在问题,因为它们会异步关闭 widget。这意味着客户端代码必须处理 widget 已关闭但尚未销毁的边缘情况。

​ 我们的Widget有所有权这个事情,根据 InitParamsownership 字段值,Widget 要么拥有其 NativeWidget,要么被 NativeWidget 拥有。下面的参数我从Google Doc里摘录出来的:

  • ownership = NATIVE_WIDGET_OWNS_WIDGET(默认): Widget 实例由其 NativeWidget 拥有。当 NativeWidget 被销毁时(响应原生销毁消息),它会在其析构函数中删除 Widget
  • ownership = WIDGET_OWNS_NATIVE_WIDGET(非默认): Widget 实例拥有其 NativeWidget。此状态意味着有其他方希望控制此对象的生命周期。当它们销毁 Widget 时,Widget 负责在其析构函数中销毁 NativeWidget。这通常用于将 Widget 放置在 std::unique_ptr<> 中或在测试中放置在栈上。

说的很干,我们如何初始化一个widget呢?

​ 下面的代码直接说明了我们如何创建一个widget

  auto* widget = new views::Widget();
  views::Widget::InitParams params(
      views::Widget::InitParams::CLIENT_OWNS_WIDGET,
      views::Widget::InitParams::TYPE_WINDOW);
  params.bounds = gfx::Rect(100, 100, 1000, 700); // 设置出现在(100, 100),宽1000高700
  // ------- delegate is creating here ----------
  // delegate = ...
  // --------------------------------------------
  params.delegate = delegate; // 我们的下一个环节在这里。。。
  widget->Init(std::move(params));
  widget->Show();

​ Widget是需要我们默认指定InitParams结构体的,那我们就需要看看,如何构造一个合法的InitParams呢?答案是看example的做法,我们看到InitParams需要指定两个枚举,我在这里列出来,直接看:

struct VIEWS_EXPORT InitParams {
    enum Type {
      TYPE_WINDOW,  // A decorated Window, like a frame window.
                    // Widgets of TYPE_WINDOW will have a NonClientView.
      TYPE_WINDOW_FRAMELESS,  // An undecorated Window.
      TYPE_CONTROL,           // A control, like a button.
      TYPE_POPUP,  // An undecorated Window, with transient properties.
      TYPE_MENU,   // An undecorated Window, with transient properties
                   // specialized to menus.
      TYPE_TOOLTIP,
      TYPE_BUBBLE,
      TYPE_DRAG,  // An undecorated Window, used during a drag-and-drop to
                  // show the drag image.
    };
	enum Ownership {
      // The client (caller) manages the lifetime of the Widget, typically via
      // std::unique_ptr<Widget>. This is the preferred ownership mode.
      //
      // If you encounter problems with this ownership mode, please file a bug.
      //
      // - The Widget remains valid even after the platform window
      //   (HWND, NSWindow, etc.) is closed.
      // - Widget API calls are safe after the platform window closes, but
      //   most will become no-ops (e.g., Show() will do nothing).
      // - The NativeWidget is destroyed when the platform window closes.
      // - When the client destroys the Widget, a close request is sent to the
      //   platform window (if it's still open).
      CLIENT_OWNS_WIDGET,

      // The NativeWidget manages the lifetime of the Widget. The Widget is
      // destroyed when the corresponding NativeWidget is destroyed.
      //
      // DEPRECATED: Prone to memory issues. A Widget* can be invalidated
      // at any time, leading to dangling pointers.  This does not fit typical
      // C++ memory management idioms.
      NATIVE_WIDGET_OWNS_WIDGET,

      // The Widget owns the NativeWidget. The NativeWidget is destroyed when
      // the corresponding Widget is destroyed.
      //
      // DEPRECATED: Causes problems with platform window shutdown. The OS
      // usually does not expect the NativeWidget to be destroyed immediately
      // when the platform window is closed. For example, if the platform window
      // has a close animation, it must remain valid until the animation
      // finishes to avoid prematurely destroying the compositor and its layer.
      // This would also cause other platform-specific issues
      // (e.g., crbug.com/40619853).
      WIDGET_OWNS_NATIVE_WIDGET,
    };

Widget::Type 枚举了 Widget 可能的不同类型,这些类型决定了 Widget 在视觉表现和行为上的特点。这些类型主要区分了窗口控件,以及窗口是否有边框装饰

类型描述特点例子
TYPE_WINDOW带装饰的窗口拥有标题栏、边框等,包含 NonClientView应用程序的主窗口
TYPE_WINDOW_FRAMELESS无装饰的窗口没有标准标题栏和边框,需要自定义绘制。媒体播放器窗口
TYPE_CONTROLUI 控件用作其他 Widget 的子组件,如按钮、文本框。按钮、滑块
TYPE_POPUP无装饰的临时窗口依附于其他窗口,失去焦点后自动关闭。下拉菜单、上下文菜单
TYPE_MENU专用的菜单窗口类似 TYPE_POPUP,但专门用于菜单。应用程序的菜单栏
TYPE_TOOLTIP工具提示窗口用于显示简短信息的无装饰小窗口。鼠标悬停在按钮上时显示的提示
TYPE_BUBBLE气泡窗口带箭头的无装饰临时窗口。聊天应用的对话气泡
TYPE_DRAG拖放窗口用于在拖放操作中显示拖动图像。拖动文件时跟随鼠标的半透明图标

Widget::Ownership 枚举定义了 Widget 与其对应的原生平台窗口 (NativeWidget) 之间生命周期管理的模式。这是理解 Widget 生命周期和避免内存问题的关键。

类型描述状态优点/问题
CLIENT_OWNS_WIDGET客户端拥有 Widget首选优点:使用 std::unique_ptr 管理,避免悬空指针,生命周期可控且健壮。Widget 对象在平台窗口关闭后依然有效,直到客户端主动销毁。
NATIVE_WIDGET_OWNS_WIDGETNativeWidget 拥有 Widget已弃用问题Widget* 指针随时可能失效,容易导致悬空指针和内存崩溃。不符合现代 C++ 内存管理实践。
WIDGET_OWNS_NATIVE_WIDGETWidget 拥有 NativeWidget已弃用问题:导致平台窗口关闭时出现问题,如关闭动画中断、合成器层过早销毁。无法保证与 OS 的正常交互。

​ 我们的任务是创建一个小窗口,于是事情变得很简单,这就是为什么我们填写了

  views::Widget::InitParams params(
      views::Widget::InitParams::CLIENT_OWNS_WIDGET,
      views::Widget::InitParams::TYPE_WINDOW);

views::WidgetDelegate

​ 这个类非常的庞大,感兴趣的朋友可以自己找找代码阅读,我们只说明感兴趣的东西。WidgetDelegate实际上讲我们对Widget的行为监视回调和行为控制专门设计的一个代理——所有的Widget行为的客制化全部都在views::WidgetDelegate进行设置。

class SimpleDelegate : public views::WidgetDelegate {
 public:
  SimpleDelegate() = default;
  ~SimpleDelegate() override = default;

  views::View* GetContentsView() override {
    if (!contents_) {
      contents_ = new SimpleView();
    }
    return contents_;
  }

  std::u16string GetWindowTitle() const override {
    return u"Simple Demo";
  }

  void assignedClosure(base::OnceClosure on_close) {
    on_close_ = std::move(on_close);
  }

  void WindowClosing() override {
    std::cerr << "Window is Closing!" << std::endl;
    if (on_close_) {
      std::move(on_close_).Run();
    }
  }

 private:
  raw_ptr<views::View> contents_ = nullptr; // raw_ptr笔者有空单独开一个博客聊聊
  base::OnceClosure on_close_; // 相当于关闭的回调闭包
};
  • GetContentsView():返回窗口的内容视图。
  • GetWindowTitle():返回窗口的标题。
  • assignedClosure(base::OnceClosure on_close):设置窗口关闭时的回调函数。
  • WindowClosing():在窗口关闭时调用,执行相关清理操作。

views::View

​ 这里才是我们需要提供内容的地方,Delegate告诉Widget你有什么,你需要画什么,有什么控件,views::View就像是其中的数据类,告诉我们画的内容本身

我整理了一下Views的文档说明,是这样的:

Viewviews 视图层级结构中的一个矩形区域,是所有视图的基类。

  • View 是其他 View 的容器(没有所谓的叶子视图——这简化了代码,减少了类型转换的麻烦和设计错误)。
  • View 包含了用于尺寸 (bounds)、布局 (flex, orientation 等)、子视图绘制事件分发的基本属性。
  • View 使用一个类似于 XUL SprocketLayout 系统的简单盒式布局管理器 (Box Layout Manager)。如有需要,也可以使用实现 LayoutManager 接口的其他布局管理器来对子视图进行布局。
  • 子类负责实现绘制存储子类特有的属性及功能。
  • 除非另有说明,views 不是线程安全的,只能在主线程访问。

View也有属性(嘿!QProperty!)

  • 旨在通过元数据动态暴露给其他子系统(如开发者工具)的属性必须遵循特定的命名、使用和实现模式。

  • 属性以其基本名称(如 "Frobble",注意首字母大写)开头。设置属性的方法必须命名为 SetXXXX,获取值的方法为 GetXXXX。例如,Frobble 属性对应 SetFrobbleGetFrobble

  • SetXXXX 方法中,更新值后,必须调用 OnPropertyChanged(),并将存储位置的地址作为键传入。此外,还可以传入任意 PropertyEffects 的组合,以确保调用所需的副作用。

  • 每个属性还应该提供一种通过注册回调函数来监听变化的方式。

  • 每个回调都使用现有的 base::Bind 机制,支持各种回调类型:对象方法、普通函数和 Lambda 表达式。

  • 对于那些旨在被其他子系统(如开发者工具或声明式布局系统)动态发现的 View 属性,每个 View 及其后代都必须包含元数据。

  • 这些子系统可以枚举任何给定实例或类的属性,并使用这些信息通过字符串来读写属性值。元数据还可用于从声明式“脚本”实例化和初始化 View(或其子类)。

  • 在每个 View 类的头文件声明中,将宏 METADATA_HEADER(<classname>, <view ancestor class>) 放置在初始的 private 部分

  • 在对应的 .cc 实现文件中,将以下宏添加到该类所在的命名空间中:

  • BEGIN_METADATA(<ClassName>)

  • ADD_PROPERTY_METADATA(<type>, <PropertyName>)

  • END_METADATA

  • 对于每个属性,在 BEGIN_METADATAEND_METADATA 宏之间,使用 ADD_PROPERTY_METADATA() 添加一个定义。

笔者研究了一下,得到了下面这个demo:

class SimpleView : public views::View {
 public:
  SimpleView() {
    SetLayoutManager(std::make_unique<views::FillLayout>());
    auto* label = new views::Label(
      u"Hello, simple Views Widget!");
    auto fonts = label->GetDefaultFontList().GetFonts();
    if(!fonts.empty()) {
      gfx::Font default_font = fonts[0];
      gfx::Font large_font(default_font.GetFontName(), 20);
      label->SetFontList(gfx::FontList(large_font));
    }
    AddChildView(label);
    SetBackground(views::CreateSolidBackground(SK_ColorWHITE));
  }
  ~SimpleView() override = default;
};

​ 这样就足够了!我们实际上创建了一个字体大小为20号的一个label放置到我们views上,默认的是我们的Label会居中显示。同时,我们的Views的背景是白色的!完事。

针对Example的一些调整

​ 我们的Window没有啥其他的资源,所以一些API会被变动一下,也就是ui::ResourceBundle::InitSharedInstanceWithLocale替代了原先的基于资源路径检索的资源加载API。

#include <windows.h>
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include "ui/aura/env.h"
#include "base/at_exit.h"
#include "base/base_switches.h"
#include "base/command_line.h"
#include "base/i18n/icu_util.h"
#include "base/lazy_instance.h"
#include "base/run_loop.h"
#include "base/test/task_environment.h"
#include "base/test/test_discardable_memory_allocator.h"
#include "build/build_config.h"
#include "base/test/test_timeouts.h"
#include "mojo/core/embedder/embedder.h"
#include "ui/accessibility/platform/ax_platform_for_test.h"
#include "ui/base/ime/init/input_method_initializer.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/ui_base_paths.h"
#include "ui/base/win/scoped_ole_initializer.h"
#include "ui/compositor/test/test_context_factories.h"
#include "ui/display/screen.h"
#include "ui/gfx/font_util.h"
#include "ui/gl/init/gl_factory.h"
#include "ui/views/background.h"
#include "ui/views/buildflags.h"
#include "ui/views/controls/label.h"
#include "ui/views/examples/example_base.h"
#include "ui/views/examples/examples_window.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/test/desktop_test_views_delegate.h"
#include "ui/views/view.h"
#include "ui/views/widget/any_widget_observer.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/views/examples/examples_color_mixer.h"
#include "ui/wm/core/wm_state.h"
#include "ui/views/widget/desktop_aura/desktop_screen.h"

// ------------------------
// 自定义 View
// ------------------------
class SimpleView : public views::View {
 public:
  SimpleView() {
    SetLayoutManager(std::make_unique<views::FillLayout>());
    auto* label = new views::Label(
      u"Hello, simple Views Widget!");
    auto fonts = label->GetDefaultFontList().GetFonts();
    if(!fonts.empty()) {
      gfx::Font default_font = fonts[0];
      gfx::Font large_font(default_font.GetFontName(), 20);
      label->SetFontList(gfx::FontList(large_font));
    }
    AddChildView(label);
    SetBackground(views::CreateSolidBackground(SK_ColorWHITE));
  }
  ~SimpleView() override = default;
};

class SimpleDelegate : public views::WidgetDelegate {
 public:
  SimpleDelegate() = default;
  ~SimpleDelegate() override = default;

  views::View* GetContentsView() override {
    if (!contents_) {
      contents_ = new SimpleView();
    }
    return contents_;
  }

  std::u16string GetWindowTitle() const override {
    return u"Simple Demo";
  }

  void assignedClosure(base::OnceClosure on_close) {
    on_close_ = std::move(on_close);
  }

  void WindowClosing() override {
    // Call the run_loop's Quit method to end the message loop.
    std::cerr << "Window is Closing!" << std::endl;
    if (on_close_) {
      std::move(on_close_).Run();
    }
  }

 private:
  raw_ptr<views::View> contents_ = nullptr;
  base::OnceClosure on_close_;
};

base::LazyInstance<base::TestDiscardableMemoryAllocator>::DestructorAtExit
    g_discardable_memory_allocator = LAZY_INSTANCE_INITIALIZER;

bool g_initialized_once = false;
int main(int argc, char* argv[]) {
  base::CommandLine::Init(argc, argv);
  TestTimeouts::Initialize();
  base::AtExitManager at_exit;
  ui::ScopedOleInitializer ole_initializer;

  base::CommandLine* command_line =
    base::CommandLine::ForCurrentProcess();
  ui::AXPlatformForTest ax_platform;

  // Disabling Direct Composition works around the limitation that
  // InProcessContextFactory doesn't work with Direct Composition, causing the
  // window to not render. See http://crbug.com/936249.
  command_line->AppendSwitch(switches::kDisableDirectComposition);

  base::FeatureList::InitInstance(
      command_line->GetSwitchValueASCII(switches::kEnableFeatures),
      command_line->GetSwitchValueASCII(switches::kDisableFeatures));


  if (!g_initialized_once) {
    mojo::core::Init();

    gl::init::InitializeGLOneOff(
        /*gpu_preference=*/gl::GpuPreference::kDefault);

    base::i18n::InitializeICU();

    ui::RegisterPathProvider();

    base::DiscardableMemoryAllocator::SetInstance(
        g_discardable_memory_allocator.Pointer());

    gfx::InitializeFonts();

    g_initialized_once = true;
  }

  base::test::TaskEnvironment task_environment(
      base::test::TaskEnvironment::MainThreadType::UI);
  auto context_factories =
      std::make_unique<ui::TestContextFactories>(false,
                                                 /*output_to_window=*/true);
  ui::ResourceBundle::InitSharedInstanceWithLocale(
      "en-US", nullptr,
      ui::ResourceBundle::LoadResources::DO_NOT_LOAD_COMMON_RESOURCES);

  ui::ColorProviderManager::Get().AppendColorProviderInitializer(
      base::BindRepeating(&views::examples::AddExamplesColorMixers));

  std::unique_ptr<aura::Env> env = aura::Env::CreateInstance();
  aura::Env::GetInstance()->set_context_factory(
      context_factories->GetContextFactory());
  ui::InitializeInputMethodForTesting();
  views::DesktopTestViewsDelegate views_delegate;
  wm::WMState wm_state;

  std::unique_ptr<display::Screen> desktop_screen =
        views::CreateDesktopScreen();
  // ------------------------
  // 创建 Widget
  // ------------------------
  base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);

  auto* widget = new views::Widget();
  views::Widget::InitParams params(
      views::Widget::InitParams::CLIENT_OWNS_WIDGET,
      views::Widget::InitParams::TYPE_WINDOW);
  params.bounds = gfx::Rect(100, 100, 1000, 700);
  auto* delegate = new SimpleDelegate();
  delegate->assignedClosure(run_loop.QuitClosure());
  params.delegate = delegate;


  widget->Init(std::move(params));
  widget->Show();

  auto disable_timeout =
        std::make_unique<base::test::ScopedDisableRunLoopTimeout>();

  run_loop.Run();

  ui::ResourceBundle::CleanupSharedInstance();
  ui::ShutdownInputMethod();
  env.reset();


  return 0;
}

​ 我们编译运行后,就能得到一个简单的窗口,带上居中的Hello, simple Views Widget!了!

Reference

1. views::Widget的介绍说明原文

////////////////////////////////////////////////////////////////////////////////
// Widget class
//
//  Encapsulates the platform-specific rendering, event receiving and widget
//  management aspects of the UI framework.
//
//  Owns a RootView and thus a View hierarchy. Can contain child Widgets.
//  Widget is a platform-independent type that communicates with a platform or
//  context specific NativeWidget implementation.
//
//  All widgets should use ownership = CLIENT_OWNS_WIDGET. The client code that
//  creates the widget should hold onto a std::unique_ptr<Widget>. The proper
//  way to close the Widget is to reset the unique_ptr.
//
//  The Close() and CloseWithReason() methods are problematic because they
//  asynchronously close the widget. This means that client code has to handle
//  the edge case of: widget is closed, but not destroyed. Use
//  MakeCloseSynchronous() to allow the client to intercept these calls
//  and reset the unique_ptr. Note that the point of
//  MakeCloseSynchronous() is to intercept calls to Close() from code in
//  //ui that client code cannot control (such as DialogDelegate). This also
//  allows client code to have a single destruction path for widgets, which
//  simplifies logic for code that should be written exactly once, such as
//  logging. If Client code does not rely on DialogDelegate or similar helpers
//  that call Widget::Close(), then MakeCloseSynchronous is unnecessary.
//
//  Aside 1: Clients are responsible for handling the case where the parent
//  widget is destroyed. There are common helpers like TabDialogManager that
//  will do this.
//
//  Aside 2: There will always be the edge case of NATIVE_WIDGET destroyed while
//  Widget is alive. This is rare and most clients do not need to handle this.
//  For clients that do care about this, the best way to detect this right now
//  is WidgetObserver::OnWidgetDestroying.
//
//  See documentation of MakeCloseSynchronous for an example.
//
//  Deprecated but kept for historical context --------------------------------
//  A special note on ownership:
//
//    Depending on the value of the InitParams' ownership field, the Widget
//    either owns or is owned by its NativeWidget:
//
//    ownership = NATIVE_WIDGET_OWNS_WIDGET (default)
//      The Widget instance is owned by its NativeWidget. When the NativeWidget
//      is destroyed (in response to a native destruction message), it deletes
//      the Widget from its destructor.
//    ownership = WIDGET_OWNS_NATIVE_WIDGET (non-default)
//      The Widget instance owns its NativeWidget. This state implies someone
//      else wants to control the lifetime of this object. When they destroy
//      the Widget it is responsible for destroying the NativeWidget (from its
//      destructor). This is often used to place a Widget in a std::unique_ptr<>
//      or on the stack in a test.

2 views::View的介绍说明原文

/////////////////////////////////////////////////////////////////////////////

//

// View class

//

//   A View is a rectangle within the views View hierarchy. It is the base

//   class for all Views.

//

//   A View is a container of other Views (there is no such thing as a Leaf

//   View - makes code simpler, reduces type conversion headaches, design

//   mistakes etc)

//

//   The View contains basic properties for sizing (bounds), layout (flex,

//   orientation, etc), painting of children and event dispatch.

//

//   The View also uses a simple Box Layout Manager similar to XUL's

//   SprocketLayout system. Alternative Layout Managers implementing the

//   LayoutManager interface can be used to lay out children if required.

//

//   It is up to the subclass to implement Painting and storage of subclass -

//   specific properties and functionality.

//

//   Unless otherwise documented, views is not thread safe and should only be

//   accessed from the main thread.

//

//   Properties ------------------

//

//   Properties which are intended to be dynamically visible through metadata to

//   other subsystems, such as dev-tools must adhere to a naming convention,

//   usage and implementation patterns.

//

//   Properties start with their base name, such as "Frobble" (note the

//   capitalization). The method to set the property must be called SetXXXX and

//   the method to retrieve the value is called GetXXXX. For the aforementioned

//   Frobble property, this would be SetFrobble and GetFrobble.

//

//   void SetFrobble(bool is_frobble);

//   bool GetFrobble() const;

//

//   In the SetXXXX method, after the value storage location has been updated,

//   OnPropertyChanged() must be called using the address of the storage

//   location as a key. Additionally, any combination of PropertyEffects are

//   also passed in. This will ensure that any desired side effects are properly

//   invoked.

//

//   void View::SetFrobble(bool is_frobble) {

//     if (is_frobble == frobble_)

//       return;

//     frobble_ = is_frobble;

//     OnPropertyChanged(&frobble_, kPropertyEffectsPaint);

//   }

//

//   Each property should also have a way to "listen" to changes by registering

//   a callback.

//

//   base::CallbackListSubscription AddFrobbleChangedCallback(

//       PropertyChangedCallback callback);

//

//   Each callback uses the the existing base::Bind mechanisms which allow for

//   various kinds of callbacks; object methods, normal functions and lambdas.

//

//   Example:

//

//   class FrobbleView : public View {

//    ...

//    private:

//     void OnFrobbleChanged();

//     base::CallbackListSubscription frobble_changed_subscription_;

//   }

//

//   ...

//     frobble_changed_subscription_ = AddFrobbleChangedCallback(

//         base::BindRepeating(&FrobbleView::OnFrobbleChanged,

//         base::Unretained(this)));

//

//   Example:

//

//   void MyView::ValidateFrobbleChanged() {

//     bool frobble_changed = false;

//     base::CallbackListSubscription subscription =

//       frobble_view_->AddFrobbleChangedCallback(

//           base::BindRepeating([](bool* frobble_changed_ptr) {

//             *frobble_changed_ptr = true;

//           }, &frobble_changed));

//     frobble_view_->SetFrobble(!frobble_view_->GetFrobble());

//     LOG() << frobble_changed ? "Frobble changed" : "Frobble NOT changed!";

//   }

//

//   Property metadata -----------

//

//   For Views that expose properties which are intended to be dynamically

//   discoverable by other subsystems, each View and its descendants must

//   include metadata. These other subsystems, such as dev tools or a

//   declarative layout system, can then enumerate the properties on any given

//   instance or class. Using the enumerated information, the actual values of

//   the properties can be read or written. This will be done by getting and

//   setting the values using string representations. The metadata can also be

//   used to instantiate and initialize a View (or descendant) class from a

//   declarative "script".

//

//   For each View class in their respective header declaration, place the macro

//   METADATA_HEADER(<classname>, <view ancestor class>) in the initial private

//   section.

//

//   In the implementing .cc file, add the following macros to the same

//   namespace in which the class resides.

//

//   BEGIN_METADATA(View)

//   ADD_PROPERTY_METADATA(bool, Frobble)

//   END_METADATA

//

//   For each property, add a definition using ADD_PROPERTY_METADATA() between

//   the begin and end macros.

//

//   BEGIN_METADATA(MyView)

//   ADD_PROPERTY_METADATA(int, Bobble)

//   END_METADATA

/////////////////////////////////////////////////////////////////////////////