电梯调度的接口设计改进:
目前接口提供的信息还不够多,比如电梯的目标楼层等问题,应该增加关于电梯当前是否空闲的状态标志和电梯当前的目标楼层,这样便于同学利用原有的数据,而不是在Scheduler的实现中重写已经有的方法自己判断。第二就是请求的发送方式,我认为都改成事件的方式最好,这样scheduler只需处理相应的事件就可以记录更多信息,从而给算法提供更大的优化空间。
另外就是目前设计存在一些问题,比如主程序每次获取下一个活动时刻,而不是下一个时刻,这样设计电梯停止一定时间后到指定楼层就会出现问题,而且关于开门时间问题也存在问题,开门固定为6秒,这5秒中选定方向后想要在电梯启动前关门之前再更改方向就无法操作了,这不符合实际情况,是这次电梯框架设计的不太好的地方之一。
UI部分采用WPF实现,采用了MVVM的设计方法,将在下文中详细说明。
UI从命令行启动,加入参数-gui即可。UI和逻辑部分分离在两个线程运行,之前的设计就是多线程的,之前主线程启动逻辑部分线程后会进入等待,这里改为若使用参数启动则显示如下图所示的窗口,同时又保证了后台数据模块的运行,而对于并发访问进行了加锁处理防止并发冲突问题。
左侧为当前电梯外的状态,包括每层的请求情况和具体的乘客,右侧为电梯内的状态和电梯当前状态。下方可以定位到每个时刻,查看该时刻的状态,也就是每个状态都会保存,这样设计的优点是可以随时返回历史状态查看,缺点是状态非常多的时候开销很大,因此命令行参数还支持最大状态,就是程序执行到最大状态时仍没有执行完就停止运行。
程序使用MVVM模式设计,具体为
Model:即ElevFramework提供的Passenger,World,Elevator等类。
View:通过Xaml定义,节选部分代码如下:
<Window x:Class="ElevGUI.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:ElevGUI" Title="MainWindow" Height="350" Width="525"> <Grid Name="grid1"> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="60" /> </Grid.RowDefinitions> <ListBox ItemsSource="{Binding Path=CurrentTickView.Floors}" Grid.ColumnSpan="1"> <ListBox.ItemTemplate> <DataTemplate> <Grid Width="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=Grid, AncestorLevel=1}, Path=ActualWidth}" Height="50"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="3" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding FloorName}"/> <StackPanel> <TextBlock Text="{Binding UpText}"/> <TextBlock Text="{Binding DownText}"/> </StackPanel> </StackPanel> <ListBox Grid.Column="1" ItemsSource="{Binding Passengers}"> <ListBox.ItemsPanel> <ItemsPanelTemplate> <VirtualizingStackPanel Orientation="Horizontal" IsItemsHost="True"/> </ItemsPanelTemplate> </ListBox.ItemsPanel> <ListBox.ItemTemplate> <DataTemplate> <StackPanel> <TextBlock Text="{Binding Name}"/> <TextBlock Text="{Binding TargetFloor}"/> <TextBlock Text="{Binding Weight}"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <Rectangle Grid.Row="1" Grid.ColumnSpan="2" Fill="Black"/> </Grid> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <ListBox ItemsSource="{Binding Path=CurrentTickView.Elevators}" Grid.Column="1" Background="{x:Null}"> <ListBox.ItemsPanel> <ItemsPanelTemplate> <VirtualizingStackPanel Orientation="Horizontal" IsItemsHost="True"/> </ItemsPanelTemplate> </ListBox.ItemsPanel> <ListBox.ItemTemplate> <DataTemplate> <Canvas Width="200" Height="1200"> <StackPanel Canvas.Bottom="{Binding ShowHeight}" Width="200" Height="150" > <WrapPanel Orientation="Horizontal"> <TextBlock Text="{Binding StringFormat=目标:\{0\}, Path=TargetFloor}"/> <TextBlock Text="{Binding StringFormat=容量:\{0\}, Path=FreeCapability}"/> <TextBlock Text="{Binding StringFormat=当前层:\{0\}, Path=CurrentFloor}"/> <TextBlock Text="{Binding StringFormat=当前高:\{0\}, Path=CurrentHeight}"/> </WrapPanel> <ListBox ItemsSource="{Binding Passengers}" Height="100"> <!--ItemsPanelTemplate> <WrapPanel Orientation="Horizontal" IsItemsHost="True"/> </ItemsPanelTemplate--> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Vertical"> <TextBlock Text="{Binding Name}"/> <TextBlock Text="{Binding TargetFloor}"/> <TextBlock Text="{Binding Weight}"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </StackPanel> </Canvas> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <StackPanel Grid.Row="1" Grid.ColumnSpan="3" > <Slider Maximum="{Binding TotalTicks}" Value="{Binding CurrentTick}" HorizontalAlignment="Stretch" VerticalAlignment="Center" IsSnapToTickEnabled="True" TickPlacement="TopLeft" /> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Path=CurrentTick,StringFormat=时间:\{0\}}"/> <TextBlock Text="{Binding Path=TotalTicks,StringFormat= \/ \{0\}}"/> </StackPanel> </StackPanel> </Grid> </Window>
这就实现了界面,不过wpf功能非常强大,很多内容如模版和动画等都没有添加,用wpf设计的好处是可以方便的修改界面,比如吧每个乘客和电梯改成图片,或为电梯得动作加动画,这些都可以方便的用xaml实现,Storyboard使原本复杂的动画特效添加变的非常简单。
View Model:
就是保存的各状态,为了能响应数据绑定需求,需实现INotifyPropertyChanged方法。
public class WorldViewModel : INotifyPropertyChanged { Dictionary<int, TickViewModel> _Ticks; public int _currenttick = 0; public int _maxtick = 0; public WorldViewModel() { _Ticks = new Dictionary<int, TickViewModel>(); } private void OnPropertyChanged(string info) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(info)); } } public void AddTickView(int tick, TickViewModel model) { lock (_Ticks) { if (_Ticks.ContainsKey(tick)) _Ticks[tick] = model; else _Ticks.Add(tick, model); _maxtick = Math.Max(_maxtick, tick); } OnPropertyChanged("TotalTicks"); } public int TotalTicks { get { return _maxtick; } } public int CurrentTick { get { return _currenttick; } set { if (value <= 0) value = 0; lock (_Ticks) { while (!_Ticks.ContainsKey(value) && value > 0) { value--; } } if (_currenttick != value) { _currenttick = value; } OnPropertyChanged("CurrentTick"); OnPropertyChanged("CurrentTickView"); } } public TickViewModel CurrentTickView { get { lock (_Ticks) { if (_Ticks.ContainsKey(_currenttick)) return _Ticks[_currenttick]; return null; } } } public event PropertyChangedEventHandler PropertyChanged; }
若电梯允许不能停靠的楼层,则在xml文件中必须保存电梯不能停靠的楼层的信息,同时对于TargetCheck函数也要检查目标楼层是否为DisabledFloor,安排乘客上电梯和程序开始时要进行数据有效性检查,看是否乘客目标位置就已经不满足DisabledFloor条件,若该层不能停靠,则不能允许电梯开门是给乘客安排到此电梯中。对于Scheduler的算法则修改非常简单,目前我采用自定义的RequestInfo类中HasRequestAbove/Below和MinRequestAbove/MaxRequestBelow等属性实现,每次执行过程中根据当前所有请求计算,只需每次计算时排除外部方向请求在禁止停靠集合中的楼层即可实现。