1

我正在尝试使用 Windows 窗体和用户控件,到目前为止,这只是一个令人头疼的问题。我不能将表单或控件设为静态,因为设计师不喜欢这样,当我在表单和控件上使用 Singleton 时,设计师仍然会向我抛出错误。

我的主窗体:

public partial class FormMain : Form
{
    private static FormMain inst;

    public static FormMain Instance
    {
        get
        {
            if (inst == null || inst.IsDisposed)
                inst = new FormMain();
            return inst;
        }
    }

    private FormMain()
    {
        inst = this;
        InitializeComponent();
    }

主屏幕.cs:

public partial class MainScreen : UserControl
{
    private static MainScreen inst;

    public static MainScreen Instance
    {
        get
        {
            if (inst == null || inst.IsDisposed)
                inst = new MainScreen();
            return inst;
        }
    }

    private MainScreen()
    {
        inst = this;
        InitializeComponent();
    }

如果 MainScreen 的构造函数是公共的,则程序运行,但是当我将其更改为私有时,我现在在 FormMain.Designer.cs 中收到一个错误,说“'Adventurers_of_Wintercrest.UserControls.MainScreen.MainScreen()' 由于其保护级别而无法访问” . 它指向这一行:

        this.controlMainScreen = new Adventurers_of_Wintercrest.UserControls.MainScreen();

我认为这是设计者默认创建的类的实例。我应该抛弃设计师吗?或者有没有办法解决这个问题?或者是否有另一种方法可以在不使用 Singleton 的情况下使类属性可访问(因为我似乎无法将表单或控件设为静态)?任何帮助将不胜感激。

4

3 回答 3

3

如果要访问实例化表单的公共属性,则需要保留对每个表单的每个实例的引用。

一种方法是为每种类型的表单创建一个带有静态变量的类:

class FormReferenceHolder
{
    public static Form1 form1;
    public static Form2 form2;
}

这样,您可以在实例化表单时设置静态变量,然后您可以从程序中的任何位置访问该变量。如果表单尚不存在,您可以更进一步,并使用设置表单的属性:

class FormReferenceHolder
{
    private static Form1 form1;
    public static Form1 Form1
    {
        get
        {
            if (form1 == null) form1 = new Form1(); 
            return form1 ;
        }
    }
}

...

static void Main()
{
    Application.Run(FormReferenceHolder.Form1 );
}
于 2013-05-10T17:04:51.687 回答
1

我想我已经回答了一个关于这个问题的问题,看起来这就是你开始走这条路的原因。第一点是我并不是专门推荐这种模式,只是想教你更多关于软件开发人员如何管理范围的知识。

也就是说,您面临的问题并非无法克服。例如,您可以通过在运行时而不是在设计时抛出异常来阻碍公共构造函数,并修改 Program.cs 以使用静态实例而不是手动构造表单。

但。

正如我在另一个问题中所说,更好的选择是更改架构,这样您就不需要库代码来直接操作 GUI。

您可以通过让 GUI 在它认为需要新数据(简单函数)时向库提出问题或在需要更改某些内容时通知 GUI 来做到这一点。任何一种方法都比让图书馆直接摆弄标签要好。

一个好的起点是类似于 MVC(模型-视图-控制器)架构,我在之前的回答中提到过。不过,最好让我们更详细地了解您的高级程序结构现在是什么样子。您在系统中使用的主要类是什么(不仅仅是您目前提到的那些)?每个人的主要职责是什么,每个人住在哪里?那么我们的建议可能会更具体一些。

编辑

因此,根据您的评论,我已经模拟了一个可能的替代架构的快速演示。

我的项目中有以下内容:

  • FormMain (Form)
  • TitleScreen (UserControl)
  • InGameMenu (UserControl)
  • MainScreen (UserControl)
  • GameController (Class)
  • GameModel (Class)

我暂时没有使用Dateand LoadSave

FormMain只是有一个每个UserControl放在它上面的实例。没有特殊代码。

GameController是一个单例(因为您已经尝试使用此模式,我认为尝试使用它的工作版本会对您有所帮助),它通过操纵模型来响应用户输入。请注意:您不能直接从 GUI(这是模型视图控制器的视图部分)操作模型。它公开了一个实例,GameModel并有一堆方法可以让你执行游戏操作,如加载/保存、结束回合等。

GameModel是存储所有游戏状态的地方。在这种情况下,这只是一个日期和一个回合计数器(好像这将是一个回合制游戏)。日期是一个字符串(在我的游戏世界中,日期以“Eschaton 23, 3834.4”的格式显示),每一轮都是一天。

为了清楚起见,TitleScreen 和 InGameMenu 各只有一个按钮。理论上(不是实现),TitleScreen 允许您开始一个新游戏,而 InGameMenu 允许您加载现有游戏。

因此,随着介绍的结束,这里是代码。

游戏模型:

public class GameModel
{
    string displayDate = "Eschaton 23, 3834.4 (default value for illustration, never actually used)";

    public GameModel()
    {
        // Initialize to 0 and then increment immediately. This is a hack to start on turn 1 and to have the game
        // date be initialized to day 1.
        incrementableDayNumber = 0;
        IncrementDate();
    }

    public void PretendToLoadAGame(string gameDate)
    {
        DisplayDate = gameDate;
        incrementableDayNumber = 1;
    }

    public string DisplayDate
    {
        get { return displayDate; }
        set
        {
            // set the internal value
            displayDate = value;

            // notify the View of the change in Date
            if (DateChanged != null)
                DateChanged(this, EventArgs.Empty);
        }
    }

    public event EventHandler DateChanged;

    // use similar techniques to handle other properties, like 


    int incrementableDayNumber;
    public void IncrementDate()
    {
        incrementableDayNumber++;
        DisplayDate = "Eschaton " + incrementableDayNumber + ", 9994.9 (from turn end)";
    }
}

需要注意的事项:您的模型有一个名为DateChanged. 每当DisplayDate更改时都会触发它。当您查看属性定义时,您可以看到这是如何发生的:set如果有人在听,访问器(您不会从 GUI 调用)会引发事件。还有用于存储游戏状态和方法的内部字段GameController(不是您的 GUI)将根据需要调用。

游戏控制器看起来像这样:

public class GameController
{
    private static GameController instance;
    public static GameController Instance
    {
        get
        {
            if (instance == null)
                instance = new GameController();

            return instance;
        }
    }

    private GameController() 
    {
        Model = new GameModel();
    }

    public void LoadSavedGame(string file) 
    {
        // set all the state as saved from file. Since this could involve initialization
        // code that could be shared with LoadNewGame, for instance, you could move this logic
        // to a method on the model. Lots of options, as usual in software development.
        Model.PretendToLoadAGame("Eschaton 93, 9776.9 (Debug: LoadSavedGame)");
    }

    public void LoadNewGame() 
    {
        Model.PretendToLoadAGame("Eschaton 12, 9772.3 (Debug: LoadNewGame)");
    }

    public void SaveGame() 
    {
        // to do 
    }

    // Increment the date
    public void EndTurn()
    {
        Model.IncrementDate();
    }

    public GameModel Model
    {
        get;
        private set;
    }
}

在顶部,您会看到单例实现。然后是构造函数,它确保始终有一个模型,以及加载和保存游戏的方法。GameModel(在这种情况下,即使加载了新游戏,我也不会更改实例。原因是它GameModel有事件,我不希望侦听器必须在这个简单的示例代码中拆线和重新连接它们。您可以决定如何您想自己解决这个问题。)请注意,这些方法基本上实现了您的 GUI 可能需要对游戏状态执行的所有高级操作:加载或保存游戏、结束回合等。

现在剩下的很容易了。

标题画面:

public partial class TitleScreen : UserControl
{
    public TitleScreen()
    {
        InitializeComponent();
    }

    private void btnLoadNew(object sender, EventArgs e)
    {
        GameController.Instance.LoadNewGame();
    }
}

游戏内菜单:

public partial class InGameMenu : UserControl
{
    public InGameMenu()
    {
        InitializeComponent();
    }

    private void btnLoadSaved_Click(object sender, EventArgs e)
    {
        GameController.Instance.LoadSavedGame("test");
    }
}

注意这两个除了调用控制器上的方法之外什么都不做。简单的。

public partial class MainScreen : UserControl
{

    public MainScreen()
    {
        InitializeComponent();

        GameController.Instance.Model.DateChanged += Model_DateChanged;

        lblDate.Text = GameController.Instance.Model.DisplayDate;
    }

    void Model_DateChanged(object sender, EventArgs e)
    {
        lblDate.Text = GameController.Instance.Model.DisplayDate;
    }

    void Instance_CurrentGameChanged(object sender, EventArgs e)
    {
        throw new NotImplementedException();
    }

    private void btnEndTurn_Click(object sender, EventArgs e)
    {
        GameController.Instance.EndTurn();
    }
}

这涉及更多一点,但不是很复杂。关键是,它将DateChanged事件连接到模型上。这样可以在日期增加时通知它。我还在这里的一个按钮中实现了另一个游戏功能(结束回合)。

如果你复制它并运行它,你会发现游戏日期是从很多地方操纵的,并且标签总是正确更新的。最重要的是,您的控制器和模型实际上对视图一无所知——甚至不知道它基于 WinForms。您可以在 Windows Phone 或 Mono 上下文中轻松使用这两个类,就像其他任何东西一样。

这是否阐明了我和其他人一直试图解释的一些架构原则?

于 2013-05-10T16:58:36.627 回答
0

本质上,问题是当应用程序运行时,它会尝试实例化主窗体窗口。但是通过使用单例模式,您实际上是在禁止应用程序这样做。

看看这里的示例代码:http: //msdn.microsoft.com/en-us/library/system.windows.forms.application.aspx

您会特别注意到本节:

[STAThread]
public static void Main()
{
    // Start the application.
    Application.Run(new Form1());
}

请注意程序如何尝试实例化Form1. 您的代码说,不,我真的不想要那样,因为您将构造函数标记为私有(静态表单也是如此)。但这与 Windows 窗体的工作方式背道而驰。如果你想要一个singleton窗体窗口,就不要再制作了。就那么简单。

于 2013-05-10T16:29:04.173 回答