使用编程方式创建Cocoa应用程序(OS X)的初始窗口

29

通常我制作iOS应用程序,但现在我尝试制作OS X应用程序,但是一开始就迷失了。我的iOS应用程序的风格完全是以编程方式为主,没有xib文件或类似的东西,因为通过键入可以更加自由地控制。然而,在OS X编程中,需要从一些带有菜单项和默认窗口的xib文件开始。菜单项中有很多项目,所以这可能不是我想要处理的东西,但我想通过编程自己创建第一个窗口。

所以我做了这件事:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{    
    NSUInteger windowStyleMask = NSTitledWindowMask|NSResizableWindowMask|NSClosableWindowMask|NSMiniaturizableWindowMask;
    NSWindow* appWindow = [[NSWindow alloc] initWithContentRect:NSMakeRect(200, 200, 1280, 720) styleMask:windowStyleMask backing:NSBackingStoreBuffered defer:NO];
    appWindow.backgroundColor = [NSColor lightGrayColor];
    appWindow.minSize = NSMakeSize(1280, 720);
    appWindow.title = @"Sample Window";
    [appWindow makeKeyAndOrderFront:self];
    _appWindowController = [[AppWindowController alloc] initWithWindow:appWindow];
    [_appWindowController showWindow:self];
}

所以在这里,我首先创建了一个窗口,并使用该windowController来初始化此窗口。窗口以这种方式显示,但我只能在此处指定内部元素,如按钮和标签,而不是在windowController中指定。这让我感到很糟糕,所以我尝试了另一种方法。

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    _appWindowController = [[AppWindowController alloc] init];
    [_appWindowController showWindow:self];
}

之后我想在 windowController 的 loadWindow: 函数中设置其他元素,如下所示:

- (void)loadWindow
{
    [self.window setFrame:NSMakeRect(200, 200, 1280, 720) display:YES];
    self.window.title = @"Sample window";
    self.window.backgroundColor = [NSColor lightGrayColor];
    
    NSButton* sampleButton = [[NSButton alloc] initWithFrame:NSRectFromCGRect(CGRectMake(100, 100, 200, 23))];
    sampleButton.title = @"Sample Button!";
    [sampleButton setButtonType:NSMomentaryLightButton];
    [sampleButton setBezelStyle:NSRoundedBezelStyle];
    [self.window.contentView addSubview:sampleButton];
    NSLog(@"Loaded window!");
    [self.window makeKeyAndOrderFront:nil];
}

很遗憾,这从未起作用。 loadWindow:从未被调用,也没有 windowDidLoad:。它们去哪了?

请不要问我为什么不使用nib。我希望在里面制作一些高度定制的视图,可能是OpenGL,所以我认为nib无法处理它。如果有人能帮忙,我将不胜感激。谢谢。

而且,谁知道如何从头开始以编程方式启动菜单项?

我正在使用最新的Xcode。


2
你的观点是错误的,XIBs可以处理OpenGL。苹果提供了一个OpenGL视图来在XIBs中使用。XIBs可以处理任何以编程方式创建的视图,它们只是一种封装这些项目的简单方法。 - sosborn
这对某些情况确实适用,但考虑一下iOS应用中的棋盘游戏。每个棋子都是一个带有自定义图像的视图,然后您需要将它们移动。如果窗口中的大多数视图都是高度动态的,具有动画效果,很难想象如何在不制造混乱的情况下实现它。 - wlicpsc
1
如果您正在使用OpenGL,那么您只需要一个视图,所有的元素都可以在OpenGL中完成。如果您不使用OpenGL,同样的思路也适用,您可以在XIB中拥有一个超级视图,然后通过编程方式填充该视图以显示其他视图。这样,您就可以享受使用XIB的好处,同时仍然可以通过代码控制游戏板。 - sosborn
所以你的意思是,在xib中制作一个大的自定义视图,然后在代码中以编程方式创建其他小视图?听起来很合理,尽管仍有许多关于根据窗口大小动态计算小视图大小的问题需要考虑。在macOS中,它会改变,而在iOS中则不会。 - wlicpsc
查看视图的autoResizingMask属性。或者在窗口大小发生变化时自己计算窗口大小。或者使用scrollView。无论哪种方式,这些问题都已经被解决了,你只需要找到适合你的应用程序的正确方法。 - sosborn
@wlicpsc:您可以使用Core Animation来渲染棋盘并进行动画处理,使用NSViews来渲染它并使用NSViewAnimations来实现动画效果,或者使用NSViews及其(基于Core Animation的)动画代理之间的中间点。原始的OpenGL代码是一种方式,但绝不是唯一的方式,而且代码肯定不需要成为“一团糟”。正如sosborn所说,所有这些都与是否在xib中构建视图无关。 - Peter Hosey
4个回答

49

我花了整个星期日自己挖掘这个问题。和提问者一样,我更喜欢在编写iOS和OSX时不使用nib文件(大多数情况下)或Interface Builder,而是直接操作底层。不过我确实使用NSConstraints。如果您正在处理较简单的UI,则完全没有必要避免使用IB,但是如果涉及到更复杂的UI设计,则会变得更加困难。

事实证明这很容易做到,为了方便“社区”,我想在这里发布一个简洁而最新的答案。有一些旧的博客文章,我发现最有用的是Lap Cat Software。向您致敬!

以下假设已经启用ARC。将您的main()修改为以下内容:

#import <Cocoa/Cocoa.h>
#import "AppDelegate.h"

int main(int argc, const char *argv[])
{
    NSArray *tl;
    NSApplication *application = [NSApplication sharedApplication];
    [[NSBundle mainBundle] loadNibNamed:@"MainMenu" owner:application topLevelObjects:&tl];

    AppDelegate *applicationDelegate = [[AppDelegate alloc] init];      // Instantiate App  delegate
    [application setDelegate:applicationDelegate];                      // Assign delegate to the NSApplication
    [application run];                                                  // Call the Apps Run method

    return 0;       // App Never gets here.
}

请注意,其中仍然有一个 Nib (xib)。 这只是为了主菜单而存在。 看起来即使到今天(2014年),似乎也没有简单的方法来轻松设置位置0的菜单项。 那个菜单项的标题等于您的应用程序名称。 您可以使用 [NSApplication setMainMenu] 设置它右侧的所有内容,但不能设置该位置 0 的菜单项。 因此,我选择保留由 Xcode 在新项目中创建的 MainMenu Nib,并将其剥离到仅包含位置 0 的菜单项。 我认为这是一个公平的妥协,也是我可以接受的事情。当您创建菜单时,请遵循其他 Mac OSX 应用程序的基本模式。

接下来修改 AppDelegate,使其类似于以下内容:

-(id)init
{
    if(self = [super init]) {
        NSRect contentSize = NSMakeRect(500.0, 500.0, 1000.0, 1000.0);
        NSUInteger windowStyleMask = NSTitledWindowMask | NSResizableWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask;
        window = [[NSWindow alloc] initWithContentRect:contentSize styleMask:windowStyleMask backing:NSBackingStoreBuffered defer:YES];
        window.backgroundColor = [NSColor whiteColor];
        window.title = @"MyBareMetalApp";

        // Setup Preference Menu Action/Target on MainMenu
        NSMenu *mm = [NSApp mainMenu];
        NSMenuItem *myBareMetalAppItem = [mm itemAtIndex:0];
        NSMenu *subMenu = [myBareMetalAppItem submenu];
        NSMenuItem *prefMenu = [subMenu itemWithTag:100];
        prefMenu.target = self;
        prefMenu.action = @selector(showPreferencesMenu:);

        // Create a view
        view = [[NSTabView alloc] initWithFrame:CGRectMake(0, 0, 700, 700)];
    }
    return self;
 }

 -(IBAction)showPreferencesMenu:(id)sender
 {
    [NSApp runModalForWindow:[[PreferencesWindow alloc] initWithAppFrame:window.frame]];
 }

-(void)applicationWillFinishLaunching:(NSNotification *)notification
{
    [window setContentView:view];           // Hook the view up to the window
}

-(void)applicationDidFinishLaunching:(NSNotification *)notification
{
    [window makeKeyAndOrderFront:self];     // Show the window
} 

好了,现在你可以开始工作了!你可以在AppDelegate中开始工作,就像你熟悉的那样。希望这有所帮助!

更新:我不再像之前展示的那样在代码中创建菜单。我发现你可以在Xcode 6.1中编辑MainMenu.xib源文件。非常灵活,只需要稍微尝试一下就可以知道它是如何工作的。比混乱的代码更快,易于本地化!请查看图片以了解我的意思:

Edit MainMenu.xib


3
非常感谢您在这么长时间后提供如此详细的解释。我本以为几个月过去后没有人会再关心这件事了,但我现在已经把正确答案改成这个,并赞扬您的工作。再次非常感谢。 - wlicpsc
1
谢谢!这很棒。 - Marc O'Morain
我该如何找到与“MainMenu” nib 相当的东西...我很困惑?什么是 nib?!!! 哈 - maxisme
http://stackoverflow.com/questions/32865162/either-appdelgate-is-called-or-viewcontroller-is - maxisme
我有点困惑。在你的解决方案中,你引用了 viewPreferencesWindow,但是这两个在 AppDelegate 中并不存在。我该从哪里获取它们呢?当然,我可以为 view 创建一个属性,但是对于 PreferencesWindow 我感到迷失。谢谢。 - Micrified
这些只是我自己创造的一些东西...你可以根据自己的意愿处理它们。 - Cliff Ribaudo

3

1

Swift 4:

// File main.swift
autoreleasepool {
   // Even if we loading application manually we need to setup `Info.plist` key:
   // <key>NSPrincipalClass</key>
   // <string>NSApplication</string>
   // Otherwise Application will be loaded in `low resolution` mode.
   let app = Application.shared
   app.setActivationPolicy(.regular)
   app.run()
}

// File Application.swift
public class Application: NSApplication {

   private lazy var mainWindowController = MainWindowController()
   private lazy var mainAppMenu = MainMenu()

   override init() {
      super.init()
      delegate = self
      mainMenu = mainAppMenu
   }

   public required init?(coder: NSCoder) {
      super.init(coder: coder) // This will newer called.
   }

}

extension Application: NSApplicationDelegate {

   public func applicationDidFinishLaunching(_ aNotification: Notification) {
      mainWindowController.showWindow(nil)
   }
}

0

在您的AppWindowController类中覆盖-init方法以创建窗口,然后使用该窗口调用super-initWithWindow:方法(这是NSWindowController的指定初始化程序)。

但我通常同意评论者的看法,没有太多理由避免使用NIBs。


谢谢。虽然看起来大家都不愿意使用nibs。对于iOS,我曾经这样做过,但是它是不完整的,就像storyboard和segues只能做一堆有限的东西,所以我完全放弃了并转向纯代码。这对于OS X来说不也是一样吗? - wlicpsc
我没有做过iOS开发,但我知道故事板和转场比仅有的NIB文件结构更加严格。NIB文件只是可以随意重构的“冷冻干燥”的对象图。并不是你最终会在NIB文件中处理所有的事情而几乎没有代码。只是使用NIB文件不会干扰您使用代码进行必要操作,并减轻一些烦琐和繁重的编码工作。所以,为什么不用呢? - Ken Thomases
故事板与XIB不同,它们的功能更加有限。就像我在另一条评论中所说的那样,XIB只是一种封装视图/对象的简单方法。它们并不能替代视图和控制器。它们只是为了让生活更轻松而存在。你可以选择不使用它们,但你最终会写更多的代码来完成相同的任务。 - sosborn
所以我想举另一个例子,当你正在做全屏游戏时,从我看到的一切都是自定义的。在这种情况下,它是否放弃了xibs,或者它利用本地全屏模式并将单个OpenGL视图置于其中来处理所有操作? - wlicpsc
我认为你对XIB的理解存在误区。XIB不能阻止你在代码中进行任何想做的事情。把它看作是一个容器即可,不是视图,甚至不是一个对象。它只是一个序列化的对象容器,有助于代码组织和基本布局。 - sosborn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接