页面布局考虑HTML元素在浏览器窗口中的展示位置和顺序。一般情况下,一个shiny程序只需要一个展示窗口,其布局应该很简单随意。然而,shiny把这部分设计得相当复杂,提供了很多相关的函数。这些函数都是在shinyApp的ui参数内使用的,为了方便本文暂把它们称为“UI函数”。

在上一节的内容中我们知道,通过tags列表的设置我们完全可以解决HTML页面元素产生和页面布局的问题,为什么shiny还要提供辣么多的UI函数呢?为说明这一点,请比较下面两个shiny“程序”所产生的HTML代码:


## 程序1:
  shinyApp(
      ui = div(class="container"),
      server = function(input, output, session) {}
  )
  ## 程序2:
  shinyApp(
      ui = fixedPage(),
      server = function(input, output, session) {}
  )


运行上面两个“程序”,分别查看页面的源代码,发现两者的 body 内具有相同的代码。但不同的是程序2的 head 部分多了下面几行代码:


<meta name="viewport" content="width=device-width, initial-scale=1" />
  <link href="shared/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
  <script src="shared/bootstrap/js/bootstrap.min.js"></script>
  <script src="shared/bootstrap/shim/html5shiv.min.js"></script>
  <script src="shared/bootstrap/shim/respond.min.js"></script>


这就是关键。 shiny的UI函数不仅产生了相应的HTML元素,还引入、规定或解决了HTML元素的依赖关系(HTML dependency) 。所以嘛,有些UI函数你可以视而不见,但有些你还不得不了解和使用。


1



前面的例子已经看到,虽然我们可以使用div函数对HTML页面进行手工分块布局,但fixPage函数做的工作更多。很显然,Shiny页面布局引入了Bootstrap框架。有关Bootstrap介绍的网络资源非常多,这里就不详细介绍了。如果真的要好好应用shiny,我觉得先了解了解Bootstrap是很有必要的。

要理解shiny布局页面的原理并不难,其实就是bootstrapPage函数和上一篇文章讲过的tags标签。这些函数在shiny源代码包的bootstrap.R和bootstrap-layout.R中定义,很容易找到。简单起见,这里我们只讲用法。



1.1



shiny的源代码中有一个基本的bootstrapPage()函数,HTML的依赖性(CSS,js)由它确定:



bootstrapPage <- function(..., title = NULL, responsive = NULL, theme = NULL) {
      if (!is.null(responsive)) {
          shinyDeprecated("The 'responsive' argument is no longer used with Bootstrap 3.")
      }
      ## required head tags for boostrap
      importBootstrap <- function() {
          list(
              htmlDependency("bootstrap", "3.3.1",
                             c(
                                 href = "shared/bootstrap",
                                 file = system.file("www/shared/bootstrap", package = "shiny")
                             ),
                             script = c(
                                 "js/bootstrap.min.js",
                                 ## These shims are necessary for IE 8 compatibility
                                 "shim/html5shiv.min.js",
                                 "shim/respond.min.js"
                             ),
                             stylesheet = if (is.null(theme)) "css/bootstrap.min.css",
                             meta = list(viewport = "width=device-width, initial-scale=1")
              )
          )
      }
      attachDependencies(
          tagList(
              if (!is.null(title)) tags$head(tags$title(title)),
              if (!is.null(theme)) {
                  tags$head(tags$link(rel="stylesheet", type="text/css", href = theme))
              },
              ## remainder of tags passed to the function
              list(...)
          ),
          importBootstrap()
      )
  }




bootstrapPage函数是可以直接调用的,前提是:你得熟悉HTML/CSS和bootstrap,否则建议用fixedPage或fluidPage。这两个函数其实也引用了bootstrapPage函数,调用这些函数就表明我们将在整个页面中应用bootstrap框架。如果你故意避开这些函数不用,那很可能在程序运行中会发生一些抓破脑袋也想不明白的意外情况。




1.2



Bootstrap有两种基本页面布局,即固定宽度布局和流式布局,其CSS样式分别由 container 类和 container-fluid 类设置。通常情况下,在固定宽度布局中页面内容的宽度通过固定像素设置;而流式布局中的页面内容则通过屏幕宽度的百分比设置。在Shiny中,页面布局函数 fixedpage() 产生具有 container 类属性的块,而 fluidPage() 则产生 container-fluid 块。

两个函数的调用方法都是一样的:



fixedPage(..., title = NULL, responsive = NULL, theme = NULL)
  fluidPage(..., title = NULL, responsive = NULL, theme = NULL)



  • … 参数:数量不限的页面内容,逗号分隔
  • title 参数:页面标题
  • theme参数:自定义页面的CSS主题样式文件。如果不设置自定义样式,则采用shiny包安装目录下www/shared/bootstrap/css子目录中的样式表文件。如果有自定义的样式表,则需要在应用程序的主目录下建立www目录,并把css文件放到该目录下(可以再设置子目录)。
  • responsive参数:现行shiny版本已弃用该参数,因Bootstrap 3的媒体查询功能已升级。

以上参数没有一个是强制要求设置的。如果不设置任何参数,fixedPage的作用就是产生下面代码:



<div class="container"></div>



而fluidPage则获得:



<div class="container-fluid"></div>



title和theme产生的内容放置到HTML代码的 head 标签内,你可以自己尝试设置这些参数,通过查看HTML源代码检查它们的作用。




1.3



navbarPage() 函数用于产生具有顶部导航条页面,而导航条下方页面可以设为固定宽度或流式布局。



navbarPage(title, ..., id = NULL, header = NULL, footer = NULL,
             inverse = FALSE, collapsable = FALSE, fluid = TRUE, responsive = TRUE,
             theme = NULL)



  • header:所有标签页顶部均要显示的标题内容,其外部容器为div.row
  • footer:所有标签页底部均要显示的脚注内容,其外部容器为div.row
  • inverse:bootstrap黑底白字风格的顶部导航条
  • collapsable:当屏幕显示宽度小于940px时是否自动折叠导航条使所有的标签均位于导航条内。该参数对于触屏设备很有用。
  • …:所有的其他参数都被当成标签页,产生对应的导航列表项目和内容块,但只有使用tabPanel函数(后面将介绍)设置的内容可以正确显示和切换。

这是一个非常奇怪的函数,试图包揽解决一大堆问题,设计思路很不清。如果你对bootstrap navbar的要求较高,你会发现它可以设置功能少得可怜,只能进行页面内的导航而不是网站的导航。




1.4



  • basicPage:已弃用,现在用fluidPage代替
  • pageWithSidebar:已弃用,现使用fluidPage和sidebarLayout产生相同页面


2



通过xxxPage函数引入bootstrap框架文件后,页面的内容布局可以灵活设置。如果你熟悉HTML/CSS,下面的绝大多数函数可用可不用。但不管怎么样,这些都应放在xxxPage函数内,因为它们需要bootstrap,除非你手工引物相应的css和js文件。




2.1



有两个函数:headerPanel和titlePanel。它们的用法是一样的:



headerPanel(title, windowTitle = title)
  titlePanel(title, windowTitle = title)



  • title 设置内容标题
  • windowTitle 设置浏览器窗口标题,默认为内容标题

这两个函数的细微差别可以通过查看它们的函数源代码看出来。headerPanel的源代码是:



headerPanel <- function(title, windowTitle=title) {
      tagList(
          tags$head(tags$title(windowTitle)),
          div(class="col-sm-12",
              h1(title)
          )
      )
  }



而titlePanel的源代码为:



titlePanel <- function(title, windowTitle=title) {
      tagList(
          tags$head(tags$title(windowTitle)),
          h2(title)
      )
  }



  • 内容标题的标签类型不一样
  • 父容器不一样,headerPanel产生额外的div容器

总体感觉上面两个函数的灵活性太差,产生的标题千篇一律。如果你也认为是这样,可以考虑用tags直接设置,比如:



shinyApp(
      ui = fixedPage(
          tags$head(
              tags$title('窗口标题'),
              tags$style(
                  rel = 'stylesheet',
                  '.title-panel {background: #ABCDEF} ',
                  '.title-panel h2 {text-align:center; color: #FF0000}'
              )
          ),
          div(
              class='col-md-12 title-panel',
              h2('页面标题')
          )
      ),
      server = function(input, output, session) {}
  )






2.2



shiny应用程序往往只需要一个浏览器窗口,因此它绝大多数(可能是全部)“导航”都是指页面内部的标签页切换,包括前面介绍了navbarPage函数所产生的顶部导航条。



2.2.1




tabPanel(title, ..., value = NULL, icon = NULL)



设置标签页的基本函数,但不是一个简单的函数。除了设置标签页内的内容(… 参数)外还用于设置导航标签,title、value和icon是用于导航标签设置的参数。它需要在navbarPage、tabsetPanel、navbarMenu等函数内部使用,否则没有太大的意义。



2.2.2



navbarPage函数已经看过了。看一下后两个函数的参数:



tabsetPanel(..., id = NULL, selected = NULL, type = c("tabs", "pills"),
              position = c("above", "below", "left", "right"))
  navlistPanel(..., id = NULL, selected = NULL, well = TRUE, fluid = TRUE, widths = c(4, 8))



为什么把navbarPage放在这里,你看看下面代码的效果就理解了:



shinyApp(
    ui = navbarPage(
        title="导航条风格",
        tabPanel("One", icon=icon("home")),
        tabPanel("Two"),
        navbarMenu(
            title = "Three",
            tabPanel("Four"),
            tabPanel("Five")
        ),
        navlistPanel(
            title = "导航面板风格", widths = c(3, 9),
            tabPanel("One", icon=icon("home")),
            tabPanel("Two"),
            navbarMenu(
                title = "Three",
                tabPanel("Four"),
                tabPanel("Five")
            )
        ),
        h4("标签页风格"),
        tabsetPanel(
            type = "pills",
            tabPanel("One", icon=icon("home")),
            tabPanel("Two"),
            navbarMenu(
                title = "Three",
                tabPanel("Four"),
                tabPanel("Five")
            )
        ),
        tags$head(
            tags$style(".tab-content .tab-content {border: 1px solid gray; min-height:200px;}")
        )
    ),
    server = function(session, input, output) {
    }
)



运行后得到的页面如下:

页面布局容器 页面布局应用于本节_bootstrap



2.2.3



用于产生下拉式导航标签,上面的例子已经看到它的使用方法:



navbarMenu(title, ..., icon = NULL)




2.3




2.3.1



两者的代码是完全一样的,都是产生bootstrap的div.row类容器:



fixedRow <- function(...) {
      div(class = "row", ...)
  }
  fluidRow <- function(...) {
      div(class = "row", ...)
  }




2.3.2



产生bootstrap的div.col-sm-n类容器:



column(width, ..., offset = 0)



  • width:设置col-sm-n中的n值,参数没有默认值,必需设置,取值为1-12间的整数,了解bootstrap媒体查询的应该知道。
  • offset:额外设置col-sm-offset-n中的n值,参数表示该列和前一列的间距,数值的单位意义和width一样,默认值为0。



2.4



这些块的特殊之处只是预设了容器和样式,如果觉得内容太多你可以完全把它们忽略。



2.4.1




absolutePanel(..., top = NULL, left = NULL, right = NULL, bottom = NULL,
                width = NULL, height = NULL, draggable = FALSE, fixed = FALSE,
                cursor = c("auto", "move", "default", "inherit"))
  fixedPanel(..., top = NULL, left = NULL, right = NULL, bottom = NULL,
             width = NULL, height = NULL, draggable = FALSE,
             cursor = c("move", "default", "inherit"))



获取position属性分别为absolute和fixed的div容器,位置参数设置初始位置,draggable参数设置是否可以用鼠标拖动。



shinyApp(
    ui = fixedPage(
        fixedPanel(
            top = 50, right=50, width=200, draggable = TRUE, style="padding: 20px; border: 1px solid red;",
            "可以移动的框框1"
        ),
        absolutePanel(
            top = 150, right=150, width=200, draggable = TRUE, style="padding: 20px; border: 1px solid red;",
            "可以移动的框框2"
        )
    ),
    server = function(session, input, output) {
    }
)




2.4.2



条件显示区块。含condition条件参数,通过JS判断该条件以聚定显示或隐藏该区块。用户不用关心JS代码的编写。由于涉及到数据交互,我把它放到以后的文章介绍。



2.4.3




inputPanel <- function(...) {
  div(class = "shiny-input-panel",
    flowLayout(...)
  )
}



外部容器为div.shiny-input-panel;内部容器为div.shiny-flow-layout;各项内容均使用独立div容器。



2.4.4




sidebarPanel <- function(..., width = 4) {
    div(class=paste0("col-sm-", width),
        tags$form(class="well",
                  ...
        )
    )
}



外部容器为div,只接收宽度width参数;内部容器为form,接收其他所有内容和参数。



2.4.5




mainPanel <- function(..., width = 8) {
      div(class=paste0("col-sm-", width),
          ...
      )
  }



只产生一个div容器,事实上和column函数差不多。



2.4.6




wellPanel <- function(...) {
      div(class="well", ...)
  }



产生一个预设样式的块,有点像代码块,但字体不特殊设置。



3




3.1




sidebarLayout(sidebarPanel, mainPanel, position = c("left", "right"),
                fluid = TRUE)



  • 产生外部容器为div.row
  • 只能设置两栏,左右排列
  • 虽然参数为sidebarPanel和mainPanel,实际上可以使用其他方法设置div块
  • fluid参数已废



3.2




splitLayout(..., cellWidths = NULL, cellArgs = list())



  • 外部容器为div.shiny-split-layout
  • 每项内容使用独立div块,cellWidths用于设置每个子块的宽度,如不设置则按内容数量进行等分父容器



3.3



  • 外部容器为div.shiny-flow-layout
  • 每项内容使用独立div块,宽度固定为220px
  • 按屏幕显示区宽度自动折行



3.4



这种模式将出现在该函数的每项内容(参数)都单独放在一行,即垂直排列。

运行下面程序,调整浏览器窗口大小,你会发现不同布局模式的变化:



shinyApp(
      ui = fixedPage(
          tags$style(
              ".container div {border: 1px solid gray; min-height:30px;}",
              "h4 {color:red; margin-top: 20px;}"
          ),
          h4("两栏模板"),
          sidebarLayout(
              sidebarPanel("side bar panel"),
              mainPanel("main panel")
          ),
          h4("垂直分割模板"),
          splitLayout("aaaa", "bbbb", "cccc", "dddd"),
          h4("垂直排列模板"),
          verticalLayout("aaaa", "bbbb", "cccc", "dddd"),
          h4("流式(自动折行)模板"),
          flowLayout("aaaa", "bbbb", "cccc", "dddd")
      ),
      server = function(session, input, output) {
      }
  )



页面布局容器 页面布局应用于本节_bootstrap_02

     

页面布局容器 页面布局应用于本节_页面布局容器_03




Author: ZGUANG@LZU

Created: 2015-08-04 二 19:58

Emacs 24.5.1 (Org mode 8.2.10)