65.9K
CodeProject 正在变化。 阅读更多。
Home

Windows Mobile 密码管理器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (58投票s)

2009 年 1 月 12 日

CPOL

16分钟阅读

viewsIcon

178452

downloadIcon

3112

一个带有触摸屏 UI 和流体控件的密码保险箱。

PasswordSafeSource

MobilePasswordSafe/Set2.png

MobilePasswordSafe/Set3.png

MobilePasswordSafe/Set4.png

MobilePasswordSafe/Set5.png

引言

密码保险箱是一个 .NET 2.0 移动应用程序,它允许您将密码保存在您的移动设备中,并使用顶级的 AES 加密进行保护。它提供了漂亮的触摸界面和手势,并且除了输入或编辑新密码时,都可以完全脱离触控笔使用。

首先,当您下载 exe 文件时,只需将 DLL 和 exe 文件放置在您选择的任何位置即可。然后,在出现两个安全警告后,您可以启动 exe 文件。password.pws 文件是可选的,可以放在“\My Documents”文件夹中,它包含一个演示数据库供您随意使用。此数据库没有密码。

如何使用密码保险箱

第一次启动密码保险箱时,会显示欢迎屏幕,并创建一个空的默认数据库。下一步,您应该使用“更改密码”按钮为其创建一个密码。然后,您可以选择一个密码类别。选择类别后,它会显示为空,您可以使用右上角标题栏中的“+”按钮创建新条目。只需在字段中键入即可。您会注意到底部的撤销(蓝色按钮)和保存按钮会被激活。现在,您可以撤销更改或保存它们。当您注销或关闭密码保险箱时,它们会自动保存。如果您想删除密码,请转到“详细信息”,按标题栏上的“-”按钮,然后确认消息对话框。

当您处于类别列表时,“搜索”按钮会变为可用。通过打开搜索面板,您可以最小化列表内容。在此面板中,您可以像输入短信一样输入。列表中只会保留那些包含您输入文本部分的项目。

如果您想最小化类别选择,请单击标题栏上的“+”按钮来添加或删除类别。不必担心删除类别。密码不会丢失,仍然可以在“所有密码”类别中找到,并且您可以随时将类别添加回列表中。

在未来的版本中,我将添加创建和编辑自定义类别的可能性。这就是为什么所有类别都存储在 password.pws 数据库中的原因。

背景

去年十二月,我购买了一部新手机 Xperia x1。圣诞节前不久,我决定快速编写一个简单的密码保险箱,以管理我在工作和家中所拥有的各种密码。但一如既往,我无法只编写简单的程序,因此我对 .NET Compact Framework for Windows Mobile 提供的功能不满意。它看起来不美观,而且脱离触控笔很难使用。因此,我决定创建自己的花哨的控件。

由于缺乏更好的名称,我目前称它们为流体控件(Fluid Controls),但很可能,我将把它们重命名为一个听起来不错且不被侵犯版权的名称。我想 iControl 可能不行 ;-)

我开发的控件不是真正的“窗口”组件,因此它们没有自己的图形和消息系统。它们更像是 Silverlight 控件,完全是不同的。

控件的核心是 FluidHost 类。

它是 Windows 窗体和流体控件之间的连接。它监听所有事件,如鼠标或键盘事件,并将它们翻译后发送给控件。它还识别手势。

一旦识别出手势,它就会向根级控件发送一个 OnGesture 事件(它会将其重定向到子控件,依此类推)。在此事件中,控件可以决定对该事件执行某些操作,或取消该事件。

以下代码片段来自 ScrollContainer 类,它是提供滚动功能的控件的基类。

public override void OnGesture(GestureEventArgs e)
{
    if (e.IsPressed)
    {
        pressedGesture = e.Gesture;
        switch (e.Gesture)
        {
            case Gesture.Up:
            case Gesture.Down:
                e.Handled = true;
                break; 

            case Gesture.Canceled:
                e.Gesture = Gesture.None;
                e.Handled = true;
                break; 

            default:
                IsDown = false;
                CanScroll = false;
                e.Handled = true;
                break;
        }
    }
    else
    {
        switch (e.Gesture)
        {
            case Gesture.Down:
                if (e.Distance > MinAutoScrollPixelDistance)
                {
                    e.Handled = true;
                    int speed = e.PixelPerMs;
                    if (speed > 0) BeginAutoScroll(-speed);
                }
                break; 

            case Gesture.Up:
                if (e.Distance > MinAutoScrollPixelDistance)
                {
                    e.Handled = true;
                    int speed = e.PixelPerMs;
                    if (speed > 0) BeginAutoScroll(speed);
                }
                break;
        }
    }

    if (MouseGesture != null) MouseGesture(this, e);
}

您可以在密码保险箱中尝试后退手势。而不是按下“后退”按钮,您可以从左向右滑动手指并释放它,这样会产生相同的效果。

基本上,当主机收到输入事件时,它会将其重定向到根级控件,后者再将其重定向到其控件,依此类推。例外情况是 OnKeyPress 事件,该事件被重定向到顶级控件(可能是文本框),然后顶级控件将事件重定向到父控件(或不重定向)。

绘制

这与普通的 Windows 控件几乎相同,除了有一个 FluidPaintEventArgs 事件。请记住,流体控件不是 Windows 控件,因此它们没有自己的画布。因此,控件会获取主机控件的 Graphics,而 FluidPaintEventArgs 有一个 ControlBounds 矩形,用于指定控件必须在其内部绘制。由于性能原因,此区域不会被裁剪,因此有可能在此矩形之外绘制。

FluidPaintEventArgs 的另一个重要属性是 Region。它包含需要绘制的区域,与 Graphics.Clip 中的区域相同。那么,我为什么要添加一个单独的属性呢?因为每当您访问 Graphics.Clip 时,它实际上会创建 Region 的副本。因此,例如,ListBox 在检查 Item 是否在区域内时会创建各种区域。这是一个巨大的性能损失,所以我添加了 Region 属性,该属性 **不会** 创建克隆。

要检查一个矩形是否已脏需要绘制,您可以使用 Region.IsVisible(rect) 方法。

private void PaintHeader(FluidPaintEventArgs pe)
{
    Rectangle bounds = GetHeaderBounds();
    Rectangle clip = pe.ControlBounds;
    bounds.Offset(clip.X, clip.Y);
    if (pe.Region.IsVisible(bounds))
    {
        if (!EnableDoubleBuffer)
        {
            PaintHeaderUnbuffered(pe);
        }
        else
        {
            PaintHeaderContentDBuffered(pe);
        }
    }
}

列表框 (ListBox)

ListBox 继承自 ScrollContainer 类,它是所有支持触摸屏滚动的控件的基类。

它允许使用不同的数据源,如任何 IListDataTable,但目前只支持 IListIBindingListIBindingList 的优点是,当 List 发生变化(如添加或删除项,甚至修改项)时,ListBox 都会收到通知(前提是开发人员不要忘记通过调用 ResetItem() 来通知绑定列表)。

List 本身可以包含任何类型的类,简单的字符串、整数或复杂的类。

要显示 ListBox 中的项,会使用 TemplateTemplate 是一个简单的面板,具有额外的 ItemItemIndex 属性。可以使用不同的 Template 来显示不同的类。因此,您可以指定 BindTemplate 事件或重写 OnBindTemplate 方法。在第一种情况下,您会收到一个 TemplateEventArgs,其中包含需要获取模板的项,您可以在 TemplateEventArgs 的 template 属性上指定模板。在后一种情况下,OnBindTemplate 期望返回可用的模板,并带有索引、项以及默认 Template 作为参数。

如果您不使用不同的 Template,只需为 ListBox 指定一个默认的 Template 属性即可。我在密码保险箱中就是这样做的。

模板如何获取数据?

它不会自动工作,您需要手动编程。有两种实现方式。我使用的方式是重写派生 TemplateOnBindValue 和/或 OnItemUpdate 方法。

protected override void OnBindValue(object value)
{
    PWNameValue item = value as PWNameValue;
    if (item != null)
    {
        nameLabel.Text = item.Title;
        valueTb.Text = item.Value;
    }
    else
    {
        nameLabel.Text = "";
        valueTb.Text = "";
    }
}

TemplateItem 值发生更改时,会发生此方法。注意“更改”这个词!

然而,OnItemUpdate 方法会在每次准备使用 Template 时发生,而不仅仅是 Item 更改时。

protected override void OnItemUpdate(object value)
{
    DetailListBox lb = DetailListBox;
    bool isSelected = lb.SelectedItemIndex == this.ItemIndex;
    nameLabel.ForeColor = isSelected ? Theme.Current.ListSecondarySelectedForeColor : 
                          Theme.Current.ListSecondaryForeColor;
    valueTb.AllowEdit = Editable && (SelectedItemIndex == ItemIndex);
}

这在您需要根据列表框项的当前状态修改模板的某些属性时会非常有用,如上面源代码片段所示。

分配数据的另一种方法是使用 ListBoxDataBound 事件处理程序。

/// <summary>
/// Occurs after the Template was data bound
/// an the EndInit state is completed for the template.
/// </summary>
/// <example>
/// You can use this events to perform some actions that require
/// to raise an event, for instance to set the focus to a control like tbFirstName.Focus().
/// </example> 
public event EventHandler<TemplateEventArgs> DataBound; 

或者,您可以重写派生列表框的 OnDataBound 方法。在源代码中,我没有使用此选项。

最后,如果您不想使用任何模板,您可以直接绘制每个项,通过向 ListBox 添加 PaintItemPaintGroupHeader 事件处理程序,或重写派生 ListBoxOnPaintItem 方法。

我使用这种可能性来为模板绘制自定义背景。

protected override void OnPaintItem(ListBoxItemPaintEventArgs e)
{
    if (e.IsSelected)
    {
        e.BackColor = theme.ListSelectedBackColor;
        e.ForeColor = theme.ListSelectedForeColor;
    }
    else
    {
        e.BackColor = theme.ListBackColor;
        e.ForeColor = theme.ListForeColor;
    } 
    
    if (e.Item is GroupHeader)
    {
        e.BackColor = theme.ListHeaderBackColor;
        e.PaintDefault();
        e.Handled = true;
    }
    else if (e.IsSelected)
    {
        e.BorderColor = e.BorderColor;
        e.PaintHeaderBackground();
        e.PaintDefaultBorder();
        e.BackColor = Color.Transparent;
        e.PaintTemplate();
        e.Handled = true;
    }
    base.OnPaintItem(e);
}

现在,您有了一个可以显示任何类型数据的 ListBox

此外,ListBox 识别一些 Item 接口。

  • ItemHeight

    允许您为项指定不同的高度。

  • IGroupHeader

    指定一个组标题,用于分隔项,例如按字母顺序,就像它对密码那样(参见图片)。

  • IColorGroupHeader

    指定一个组标题,该标题还指定应以什么背景颜色绘制标题,而不是预定义的颜色。

向列表添加组标题的最简单方法是向列表中添加一个带有标题的 GroupHeader 类。

private NotifyList AddHeaders(NotifyList passwords)
{
    NotifyList list = new NotifyList(); 
    string current = ""; 

    foreach (PasswordData d in passwords)
    {
        string c = d.Name.Substring(0, 1).ToUpper(); 
        if (current != c)
        {
            current = c;
            list.Add(new GroupHeader(c));
        }
        list.Add(d);
    }
    list.ModifyChanged += new EventHandler(ListModifyChanged);
    return list; 
}

ListBoxDataSource 通常是一个 BindingList<object> 列表。为什么不是一个更具体的类,如 BindingList<MyData>?因为那样您就无法添加组标题。当然,您可以创建一个 MyData 类和一个派生的 MyGroupHeaderData 类,然后您可以指定一个 BindingList<MyData> 并添加一些组标题。

一旦将 IGroupHeader 作为第一个项附加,ListBox 就会添加一个永久可见的标题,显示最后一个可用的组标题。当新的组标题即将成为新标题时,它还会执行重叠。这很难解释,最好是亲自尝试一下。

ListBox 会按需显示滚动条,一旦 TopOffset 改变,滚动条就会出现,并在延迟后自动消失。

Alpha 混合

可以使用淡入淡出效果来隐藏滚动条,并且可以将标题设为透明。但是,我没有使用它,因为 alpha 混合会减慢系统速度。我将通过使用 DirectX 2D 来实现 alpha 混合功能,而不是使用 GDI+ DLL 中的方法来改进这一点。

面板 (Panel)

Panel 当然是不同控件的宿主。每个控件都有一个 Anchor 属性,该属性指定了如何在 Panel 中布局控件。目前没有 Docking 属性。它通过直接重写控件的 OnSizeChanged 事件来实现,但也许我会在稍后的版本中添加它。控件的布局很智能,因此减少了绘制被后续控件隐藏的区域的需求,除非它们是透明的。

Panel 还可以充当窗口。因此,它具有 ShowShowMaximizedClose 方法。一旦调用 Show,它就会出现在先前控件的前面,并带有指定的边界。如果调用 ShowMaximized,它将被最大化为全屏。更有趣的是向 Show 或 ShowMaximized 调用添加一个 ShowTransition 参数。此参数指定面板不应仅仅出现,而是应该以动画方式进入屏幕。有各种过渡效果,但目前只支持 FromBottomFromTop。没有 DirectX 2D,淡入淡出效果太慢了。而且,我不需要使用 FromLeftFromRight。一旦有时间,我还会添加一些其他效果,如翻页、缩放等。

void newCategoryButton_Click(object sender, EventArgs e)
{
    CategoriesPanel cp = CategoriesPanel.Instance;
    cp.UpdateData();
    cp.ShowMaximized(ShowTransition.FromBottom);
}

要加速您的面板,应该将 EnableDoubleBuffer 设置为 true。这会创建一个单独的位图和区域来使无效,因此画布仅在其自己的区域失效时才会被重绘。EnableDoubleBuffer 很智能,并不总是实际使用双缓冲。当它只包含一个已经双缓冲的控件时,使用双缓冲没有意义。相反,将图像从一个位图转移到另一个位图会导致性能下降。面板会检查情况,只有在适当的情况下,才会创建双缓冲。

此外,您可以指定一个低于 255 的 Alpha 值,并且当您同时使用 EnableDoubleBuffer 时,面板会变得透明。但请注意,alpha 混合目前很慢,面板越大,速度就越慢。

按钮组 (ButtonGroup)

ButtonGroup 将多个按钮分组在一起。您可以指定 ButtonGroup 的哪些角应该是圆角的。

标题栏 (Header)

顾名思义,Header 是一个标题控件,它包含文本、左侧的 Backbutton 和右侧的 ButtonGroup

过渡面板 (TransitionPanel)

Panel 可以包含多个 Control,但一次只能显示一个控件。您可以使用 ItemIndex 和/或 Item 属性来更改要显示的 Control。有趣的是,TransitionPanel 使用过渡来在控件之间切换。

导航面板 (NavigationPanel)

这是 HeaderTransitionPanel 的组合,它使面板的过渡与标题栏同步。它还会自动管理 BackButton,主要用于显示分层结构。

NavigationPanel 承载 Page 面板。

NavigationPanel 具有 BackForward 方法。Back 导航到上一个 PageForward 导航到下一个级别的 Page

页面控件 (PageControl)

这是一个用于导航面板的面板,并具有由 NavigationPanel 维护的额外属性。

  • BackButton 指定一个不同的后退按钮,如果它不被 NavigationPanel 维护的话。
  • Buttons 包含当页面变为活动状态时要在标题栏中显示的按钮。

请注意,我打算在页面切换时也在旧按钮和新按钮之间添加淡入淡出动画,但正如之前提到的,alpha 混合存在相同的性能问题,所以让我们看看 DirectX 能做什么……

数字键盘 (NumericPad)

这是一个复合面板,顾名思义,它构建了一个用于不同目的的数字键盘。

消息对话框 (MessageDialog)

一个预定义的面板,底部有文本和最多两个按钮。它的目的是显示带有两种选择请求的简单对话框。您可以使用 MessageDialog.Show() 方法快速显示对话框。您也可以以模态方式打开对话框,这样,屏幕上会有一个透明的空面板覆盖在其他控件之上,使其他控件看起来更暗且不可触摸。在这种情况下,您必须指定一个事件处理程序来决定按下这些按钮之一时该做什么。这里有一个示例。

void delPasswordButton_Click(object sender, EventArgs e)
{
    passwordToRemove = passwordsListBox.SelectedPassword;
    if (passwordToRemove != null)
    {
        MessageDialog dialog = new MessageDialog();
        dialog.Message = "Delete Password?";
        dialog.Result += new EventHandler<Fluid.Classes.DialogEventArgs>(dialog_Result);
        dialog.ShowModal(ShowTransition.FromBottom); 
    }
} 

void dialog_Result(object sender, Fluid.Classes.DialogEventArgs e)
{
    switch (e.Result)
    {
        case System.Windows.Forms.DialogResult.OK:
            ListBuilder.Instance.DeletePassword(passwordToRemove);
            UpdatePasswords();
            this.GoBack();
            break;
    }
} 

最后,我简要提一下

  • Button
  • 扁平化按钮 (FlatButton)
  • 文本框
  • Label

控件,它们是主要的控件。

请注意,我大多重用了控件,甚至 EventArgs。这是因为在我玩...测试滚动行为时出现了一些延迟。我发现原因是垃圾回收器。为了提高性能,我通常会创建它们,或者首先按需创建。

private void EnsurePaintEvents()
{
    if (paintEvents == null) paintEvents = new FluidPaintEventArgs();
}

动画器类 (Animator class)

这是提供精美动画的类,类似于 Silverlight,但不如它灵活。您有三种动画:LinearAcceleration/DecelerationLogAcceleration/Deceleration 顾名思义。如果您指定一个正的 Acceleration 属性,动画会加速;如果为负值,则会减速。Log(对数)动画是对数的,最适合看起来像释放橡皮筋的视觉效果。您可以看到这种动画,当您将列表框的顶部位置向下移动然后松开手指时,它就会跳回到默认位置。

.NET Compact Framework 中的 GDI+

不幸的是,我绘制控件所需的某些渲染方法在 Compact Framework 中缺失,尽管 GDI+ DLL 是 Windows Mobile 的一部分。因此,我使用了一个自定义的 GraphicsPlus 类,它以 Graphics 作为参数,构建了一个包装器来访问现在可用的方法,例如绘制形状等。Windows Mobile GDI+ 提供的所有功能并非都能真正实现。有时,您会收到一个 NotImplemented 异常,例如,当您尝试使用带有 GraphicsPathRegion 时。这不起作用,所以我不得不创建一个变通方法来创建光滑的按钮,通过实际创建路径的两个部分,一个用于顶部,一个用于底部,两者都具有不同的渐变。

设计器

由于流体控件不是窗口控件,因此没有可视化设计器可用。但由于目标是 Windows Mobile,您不会创建过于复杂的 UI,因此定义控件的代码并不复杂。

密码保险箱

关于密码保险箱本身,有几点需要说明。

它设计用于在分辨率为 480x800 像素的系统上运行,但也能在 240x320 像素的较小分辨率下运行,尽管由于字体太小,某些文本可能无法阅读。只有过滤掉密码的搜索面板不会显示。

在类别列表中,标题栏上有一个编辑按钮和一个添加 (+) 按钮。当您按下编辑按钮时,每个项的左侧会以动画方式出现一个圆形按钮,提供删除该项的选项。一旦您按下此按钮,右侧会再出现一个按钮来确认删除。这看起来很漂亮,但实际上性能并不高。(+) 按钮的功能相同,既可以添加也可以删除项。模仿 iPhone 的行为只是一个概念验证,表明实现起来并不难,将在后续版本中移除。

密码以 XML 格式保存,并使用顶级的 AES 加密进行加密。

加密

.NET Framework 提供了许多不同的对称和非对称加密算法,Compact Framework for Windows Mobile 也是如此。但 Compact Framework 中缺少一个重要类。

  • Rfc2898DeriveBytes

此类提供了从任意密码字符串创建密钥数据的功能,这是每种加密算法所必需的。您不能简单地将密码字符串转换为 byte[] 数组,例如使用 Encoding.Default.ToArray(password),因为密钥必须具有特定的长度。长度可以因不同的块而异,并且取决于您使用的算法。例如,首选的 AES 或 Rijndael 算法需要 128、256、384 等的密钥长度,但不能介于这些值之间。因此,您需要一种算法,该算法可以将任何字符串编码为至少 128 字节的字节数组,不多不少。

幸运的是,这就是哈希算法所做的!因此,作为缺失的 Rfc2898DeriveBytes 的替代品,我们可以使用 Compact Framework 中的一个哈希算法,该算法创建的长度符合加密算法的要求。在这种情况下,由于我们使用的是 AES,我们需要一个长度为 128 或 256 的哈希,最匹配的是 MD5 算法。尽管它不是最强的,SH256 会是更好的选择,但不幸的是,Compact Framework 不包含此哈希算法,所以我们需要使用 MD5。

这里有一个关于如何在 Windows Mobile 上实现加密/解密的示例。

最后的寄语

所以,在几乎 7000 行代码之后,我向您展示了我的圣诞假期项目。一旦我最终确定了控件的最终名称,我就会将它们推送到 CodePlex,以让您及时了解新版本。

我希望您喜欢我的应用程序,当然,我也希望能参加 2008 年微软移动开发者大赛 :-)

© . All rights reserved.